Scroll animation website examples, and how each works

Scroll animation website examples grouped by technique: scrubbed films, pinned tracks, traveling objects and CSS view timelines, with real values.

A tall stack of thin glass panes floating on pure black, each pane shifted a little further than the one above it like successive frames of one long scroll, their edges lit by a single amber glow.
One number, the scroll position, drives every frame.

A scroll animation website ties what moves on screen to the scroll position rather than to a clock: scroll down and the scene advances, scroll back and it runs in reverse, stop and it stops. The strong examples use one of five techniques, and each one reduces to a few numbers you can write down.

Those numbers are a distance in viewport heights, a progress value that runs from 0 to 1 across it, a mapping from progress to the thing that moves (usually linear), a smoothing time constant of roughly 100ms to 200ms where smoothing is wanted at all, and a still layout for readers who ask for reduced motion. Get those five right and any of the techniques below will feel deliberate.

The examples below come from the Thanor library, grouped by technique. Several do all of their scroll motion without an animation library, using a sticky element, one requestAnimationFrame loop and the browser's native scroll. The method for writing the prompt itself, with a distance budget per stage, is in the scroll animation prompt guide; this page is the gallery and the mechanics behind it.

Five scroll animation techniques, in values#

TechniqueWhat the scroll drivesTypical distanceSmoothing
Scrubbed filma paused video's currentTime300svh to 700svh, pinneda 110ms to 200ms chase
Pinned track or masktranslateX or clip-paththe track's overflow, 1px per 1pxnone, raw scroll
Traveling objectposition, size and tilt of one fixed elementthe whole pagechased per frame
Type tied to progressoffsets and opacity20vh to 100vhnone, linear
Reveal oncea one-off transitionnone, nothing pinsthe transition's own curve

The progress value is the same in all five. For a section taller than the window, with a child set to position: sticky; top: 0, progress is the distance scrolled past the section's top divided by the section's height minus one window: p = clamp((scrollY - top) / (height - innerHeight), 0, 1). Measure top and height on load, when the fonts are ready and on resize, never inside the animation loop, so the loop only reads scrollY and writes transforms.

The mapping is the decision that most changes how the page feels. A real curve on the progress value, or a spring, makes content arrive after the reader has stopped scrolling, which reads as lag. The wheel already supplies the easing, so the mapping from scroll to movement should usually be linear, and the only smoothing should be a short chase on media that cannot follow raw scroll, like a video seek. Onward writes this into its design: every curve on that page is linear, spelled out as cubic-bezier(0, 0, 1, 1) so the flatness reads as chosen.

Scrubbed film examples#

A scrubbed film is a video that never plays. The page loads it paused and sets its currentTime from the scroll progress on every frame, so the reader becomes the playhead. It is the most cinematic scroll technique available, because the frames can show anything a camera can: cloth sliding off a car, a watch coming apart, a camera circling an object.

Ardente pins its hero over 3.6 screens of scroll and scrubs one 10-second take of 241 frames at 24 frames a second. The dust sheet slides off the car while the camera circles from the front three-quarter to the rear three-quarter, and three captions, a timecode and a knob on a quarter arc travel with it. Scroll back and the film runs backwards frame for frame. A sideways drag on the stage scrubs too, by scrolling the page, so the drag and the scroll never disagree.

Verdier does the same over 4.6 screens with a 240-frame film: an emerald diver watch lifts off wet basalt, backs out its case screws and opens into an exploded movement. Over it sits a film-editor HUD with a chapter label, a progress bar, a timecode and a frame counter, all reading the film's time, while four chapters of copy blur in and out on cue.

Both prompts carry the three details that are easiest to leave out. Every frame of the film is a keyframe. The poster is the film's own first frame, so the hand-over from image to video never jumps. And under reduced motion nothing pins and no film loads; Ardente shows the film's last frame as its hero instead. Haymaker pushes the idea furthest, with one stage pinned over 7.2 screens that plays a whole boxing round; its sequence is taken apart in the GSAP ScrollTrigger guide.

How a scrubbed film is built#

Encode every frame as a keyframe#

A normal web video stores a full picture every second or two and only the differences in between. That is efficient for playback and bad for seeking, because every seek makes the decoder walk forward from the last full picture, so a scrubbed film stutters, and stutters worse in reverse. Encoding all-intra, with every frame a keyframe, lets each seek land at once in either direction. The file gets larger, so keep the take short (6 to 10 seconds), drop the audio track, and cut a separate portrait version for phones.

bash
ffmpeg -i take.mov -c:v libx264 -g 1 -crf 20 -pix_fmt yuv420p \
  -an -movflags +faststart film.mp4

Chase the target, and guard every seek#

js
const film = document.querySelector(".film");   // muted, playsinline, preload="auto"
const track = document.querySelector(".scrub"); // 460svh tall, its stage sticky
let top = 0, run = 1, shown = 0, last = 0, seeking = false;

const measure = () => {
  top = track.getBoundingClientRect().top + scrollY;
  run = track.offsetHeight - innerHeight;
};
addEventListener("resize", measure);
document.fonts.ready.then(measure);
film.addEventListener("seeked", () => { seeking = false; });

function tick(now) {
  const dt = Math.min(0.1, (now - last) / 1000);
  last = now;
  const p = Math.min(1, Math.max(0, (scrollY - top) / run));
  const want = p * (film.duration - 0.04);            // land inside the last frame
  shown += (want - shown) * (1 - Math.exp(-9 * dt));  // settles in about 110ms
  if (!seeking && Math.abs(film.currentTime - shown) > 0.02) {
    seeking = true;
    film.currentTime = shown;
  }
  requestAnimationFrame(tick);
}
film.addEventListener("loadedmetadata", () => { measure(); requestAnimationFrame(tick); });

The guard is the part that matters. Ardente's prompt has the shown time chase the target with a time constant of about 110ms, seek only when the film is more than 20ms away, and never issue a seek while one is still in flight. Lacca uses a slower 200ms constant so the picture glides on for a moment after each stroke, and snaps instead of fast-forwarding when the target jumps by more than 40% of the film, as it does when someone presses End. The constant is taste. The guard is required.

Run the check for prefers-reduced-motion: reduce before any of this, and when it matches, never give the video a source at all. The reader gets the last frame as an image and the bandwidth of one still.

Pinned tracks, masks and a traveling object#

Lacca holds still twice while a film moves. Its hero is pinned over four screens and scrubs a film of a hand with glossy lacquered nails reaching into the lens. Then a row of six bottles slides sideways one pixel for each pixel scrolled, and the pin lasts exactly as long as the row overflows the window. When the last card meets the right edge, a full-height slit opens from the center over one and a half screens, a clip-path: inset(0 X) mask, onto a second film scrubbed by the opening itself.

Palenque has no pin for its signature move. One cut-out of the mezcal bottle is fixed to the viewport, and a single loop chases five values for it as you scroll: its center x and y, its height, its tilt and its tip. The bottle starts lying at 19.75° across two giant words, stands itself up over the first 1.1 screens, holds the center while four bands pass behind it, then shrinks and flies into the empty slot of a row of five bottles. The words and splashes around it move at their own speeds, which is covered in the parallax examples.

Onward shows how little a type-driven scroll needs. The hero section is 120vh tall with a 100vh sticky overlay, which leaves 20vh of travel. Across it, four words per side start 60, 100, 140 and 180px off their places and slide home, the outermost furthest, while both columns go from 0.35 to full opacity. The only transition on the words is transform 50ms linear, short enough to smooth the steps of a mouse wheel and too short to read as an animation.

Lacca and Palenque hang every scroll-linked layout off one class that the head script adds only when the reader has not asked for reduced motion; without it, each page takes a static layout that still carries every word and picture. Onward does the same with a media query, leaving its words settled at full opacity.

Reveals that fire once#

Most scroll animation on most pages should be this: an element animates in place the first time it enters and never again. Nothing pins, so the reader never loses their place, and it is the technique the motion design guide gives its durations for. Scrolltide, a paid prompt library, publishes its house values for entrances like these, and they are worth reading before you set yours.

The trigger is an IntersectionObserver. Its threshold is the share of the element that must be visible before the callback runs: 0 fires as soon as the element touches the viewport, 1 waits for every pixel, and an array such as [0, 0.25, 0.5] fires at each step, as MDN documents. rootMargin grows or shrinks the viewport's box before the test, in px or %, so "0px 0px -10% 0px" makes elements wait until they clear the bottom tenth of the screen.

js
const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (!e.isIntersecting) continue;
    e.target.classList.add("is-in");
    io.unobserve(e.target);           // once, never on the way back up
  }
}, { threshold: 0.3 });
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));
css
.motion .reveal img {
  clip-path: inset(12% 0 0 0);
  scale: 1.08;
  transition: clip-path 1200ms cubic-bezier(0.16, 1, 0.3, 1),
              scale 1600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.motion .reveal.is-in img { clip-path: inset(0); scale: 1; }

Those values follow Ardente's workshop photographs: when 30% of a photograph is in view it unclips from 12% at the top over 1200ms and settles from a scale of 1.08 over 1600ms, both on cubic-bezier(0.16, 1, 0.3, 1), while its figure counts up over 1400ms. Unobserve after the first entry, or the reveal replays every time the reader scrolls back up.

CSS scroll-driven animations in every engine#

Since Firefox 159 shipped them on 27 October 2026, CSS scroll-driven animations work in every major engine: Chrome and Edge from version 115, Safari from 26 and Firefox from 159, according to MDN's compatibility data, which marks the feature Baseline 2026, newly available. That changes what a reveal or a progress bar costs, because neither needs a script anymore.

There are two kinds of timeline. animation-timeline: scroll() maps a scroll container's whole range to the animation, which suits a reading progress bar, and scroll(root) uses the document. animation-timeline: view() maps an element's own passage through the viewport, and animation-range picks the slice of that passage you want: entry, exit, contain or cover, each with percentages.

css
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .progress {
      transform-origin: 0 50%;
      animation: grow linear both;
      animation-timeline: scroll(root);
    }
    .reveal {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 35%;
    }
  }
}
@keyframes grow { from { scale: 0 1; } }
@keyframes rise { from { opacity: 0; translate: 0 24px; } }

MDN flags one trap: the animation shorthand resets animation-timeline, so the timeline has to be declared after it. Chrome's documentation gives the reason to prefer it over a scroll listener: scroll-driven animations can run off the main thread, staying in sync with the scroll, where a listener receives scroll events asynchronously because the browser scrolls on a separate process.

What CSS cannot do is seek a video, smooth a value over time, or branch on logic. A view timeline is the right tool for reveals, progress bars and parallax layers, and the wrong one for anything with a playhead, which still needs the short loop above.

What a scroll animation prompt has to specify#

A model asked to "add smooth scroll animations" picks a library, a curve and a distance on your behalf, and they are the same picks it makes for everyone, which is most of why AI websites look the same. A prompt that produces one of the examples above names seven things.

  1. The technique, by name: scrubbed film, pinned track, traveling object, type tied to progress, or reveal once.
  2. The distance of every pinned stage in svh or lvh, chosen on purpose, so a phone's collapsing address bar does not cut a sequence short.
  3. The progress formula and the mapping, written as linear unless there is a reason not to be.
  4. What progress drives, with from and to values: currentTime from 0 to the duration minus 0.04s, translateX from 0 to minus the overflow, opacity from 0.35 to 1.
  5. The smoothing, as a time constant in ms, applied only to media.
  6. The media spec: all-intra, 24 frames a second, the poster as the first frame, a portrait cut for phones.
  7. The still state: what the page shows under reduced motion and without scripts.
text
Hero: a 460svh section with a 100svh sticky stage. Progress p runs 0 to 1
across it, linear. A muted all-intra film (24 fps, every frame a keyframe)
shows time p * (duration - 0.04s), chased with a 110ms time constant;
seek only when 20ms off and never during a seek. Captions fade in over
p 0.08 to 0.16 and out over 0.30 to 0.36. Sticky positioning and one
requestAnimationFrame loop, no animation library. Under
prefers-reduced-motion nothing pins and the hero is the film's last frame.

That fragment was written for this page as an illustration. A full Thanor prompt runs through the palette, the type scale, every section and the hosted media files as well, and the distance budget per stage is the subject of the scroll animation prompt.

Scroll animation mistakes to avoid#

  • Hijacking the wheel. Canceling the native scroll and animating the page yourself breaks trackpads, keyboard scrolling and find in page. Every example above uses the browser's own scroll.
  • Putting a curve on the progress. Content that eases toward the scroll position arrives after the reader stops, which reads as lag. Keep the mapping linear and smooth only media.
  • Leaving sparse keyframes in a scrubbed film. The seek walks forward from the last keyframe and the scrub stutters. Encode with -g 1.
  • Measuring before the fonts load. A pin measured against a fallback face starts in the wrong place once the real face swaps in. Measure again on document.fonts.ready.
  • Pinning everything. One signature pin, two at most, and reveals for the rest. Each extra pin is a stretch where the reader cannot see their position in the page.
  • Animating layout properties. Move with transform, translate and scale, hide with opacity and clip-path, and leave top, width and box-shadow alone. The animation budgets list the rest.
  • No still state. Under prefers-reduced-motion: reduce the pins go, the films become stills and the copy sits in place. The reader keeps every word.

Pick one technique, then open the example#

Choose the technique from what the page sells. One object deserves a scrubbed film. A range of products suits a pinned track. A single product that should stay in view across the page suits a traveling object, and a statement suits type tied to progress. A long editorial story that alternates text and pinned scenes is its own format, covered in the scrollytelling guide, and a hero that simply loops a film belongs in the video background guide. The rest of this cluster lives on the inspiration hub, and the other effects, from text reveals to cursor-driven films, are in website animation effects.

Open Ardente, watch the sheet come off in its preview, and copy its prompt into Claude, Cursor or Lovable: the scroll distance, the all-intra encoding, the still state and the hosted film the page uses are already written. Ardente is on the paid plans, listed on the pricing page. To read the prompt format before you decide, Vesperion is free with an account, and the rest of the library is one click from either.

Questions this raises

What is a scroll animation on a website?

A scroll animation ties a visual change to the scroll position instead of a clock. Scroll down and it advances, scroll back and it reverses, stop and it stops. The common forms are a video scrubbed by the scroll, a pinned section whose contents change while it holds, an object that travels between sections, and a reveal that plays once as an element enters the screen.

Can you make scroll animations with CSS only?

Yes, for reveals, progress bars and parallax layers. animation-timeline with scroll() or view() links a CSS animation to scrolling, and since Firefox 159 shipped it in October 2026 it works in Chrome, Edge, Safari and Firefox. CSS cannot seek a video or smooth a value over time, so a scrubbed film still needs a short script.

Do scroll animations slow down a website?

Not when they animate transform, opacity and clip-path, read layout measurements once instead of every frame, and run from one requestAnimationFrame loop or a CSS view timeline. What slows pages down is animating width, top or box-shadow, listening to scroll events that write layout, and loading a heavy film before the first screen needs it.

Why does my scroll-scrubbed video stutter?

Almost always because the video has sparse keyframes. Normal web encodes store a full picture every second or two, so each seek makes the decoder walk forward from the last one, and reverse scrubbing is worse. Re-encode with every frame as a keyframe, seek only when the target is more than about 20ms away, and never start a seek while one is in flight.

Part of

Inspiration, with the prompt

Examples of the looks and effects people search for, from 3D scenes to scroll animation, each one shown working and paired with the specification that builds it.