Scroll pinning

Matt Perry
In this tutorial, we're going to build the Scroll pinning example step-by-step.
This tutorial is rated beginner difficulty, which means we'll spend some time explaining the Motion APIs that we've chosen to use (and why), and also any browser APIs we encounter that might be unfamiliar to beginners.
Here's a live demo of the example we're going to be creating:
Introduction
The Scroll pinning example shows how to create a horizontal scrolling gallery that moves as you scroll vertically down the page. The gallery "pins" in place while scrolling, revealing images one by one, and includes a progress bar that fills as you progress through the images.
This example uses the scroll function from Motion to link scroll position to animations, along with the animate function to define what properties should change. Motion makes it easy to create scroll-linked animations without writing complex scroll event listeners or calculating percentages manually.
Get started
Let's build a horizontal photo gallery. We'll create the HTML structure with a header, image container, footer, and progress bar:
The structure includes five photo containers arranged in a flexbox list. The key to making this work is the combination of HTML structure and CSS positioning.
Let's animate!
The CSS pinning technique
Before adding Motion animations, we need to understand how scroll pinning works with CSS. The technique uses three key elements working together:
The .img-group-container has a large height (500vh - five times the viewport height). This creates the scrollable area:
Inside it, a div uses position: sticky to stay fixed in the viewport while you scroll through the container:
The sticky positioning means the element stays in place relative to the viewport as long as you're scrolling within the .img-group-container. This creates the "pinned" effect - the gallery stays visible while you scroll vertically.
The .img-group contains all our images laid out horizontally:
Each image container is exactly one viewport width (100vw), so five images side by side create a 500vw wide row.
Import from Motion
Now let's import the Motion functions we need to bring this gallery to life:
The animate function creates animations, and the scroll function links those animations to scroll progress.
Count the images
First, we need to know how many images we have so we can calculate how far to move the gallery:
Animate the gallery horizontally
Here's where the magic happens. We'll use the scroll function to move the gallery horizontally as the user scrolls vertically:
Let's break this down:
The scroll function takes two arguments. The first is an animation created with animate. The second is a configuration object that tells Motion which element to track for scroll progress.
The animate function targets .img-group and animates its transform property. The value is an array representing the start and end states: ["none", "translateX(-400vw)"].
Why -400vw? With five images, we need to move four viewport widths to the left to show all of them. The first image is visible at 0vw, and by the time we've scrolled to the bottom of the container, we want the last image visible, which means moving ${items.length - 1}00vw (that's 400vw) to the left.
The target option tells Motion to track scroll progress within the .img-group-container element, not the entire page. This means the animation only happens while scrolling through our tall container.
As you scroll down through the 500vh tall container, Motion smoothly interpolates the horizontal position from 0vw to -400vw, creating the illusion that you're scrolling horizontally.
Add the progress bar
Finally, let's add a progress indicator that shows how far through the gallery you've scrolled:
This works the same way as the gallery animation, but instead of translating position, we're scaling the progress bar from 0 (invisible) to 1 (full width). The bar grows from left to right as you scroll through the gallery.
The progress bar is styled with transform: scaleX(0) initially and positioned at the bottom of the screen:
Both scroll animations use the same target, so they stay perfectly synchronized as you scroll.
Conclusion
We've built a horizontal scrolling gallery controlled by vertical scroll using Motion's scroll and animate functions. The CSS position: sticky technique creates the pinned effect, while Motion handles the smooth animation of the gallery's horizontal position and the progress bar's width. This pattern works great for photo galleries, timelines, or any content you want to reveal gradually as users scroll.


