Scroll-driven video: turning timelines into scroll positions
CSS scroll-driven animations finally went baseline in 2026. A practical tutorial on mapping video timelines to scroll positions, when to use scroll vs video, and the hybrid pattern we use on hyperframes.dev.
Scroll-driven animations have been "coming soon" for so long that I had stopped tracking them. Then a few weeks ago I went to add a fallback for an animation on our marketing site, and I noticed the fallback was no longer needed. The animation-timeline property and the scroll() / view() timeline functions are now Baseline 2026 — supported in every modern browser, no flags, no polyfills.
That is a quiet milestone, but for anyone doing motion on the web it changes a lot. This post is a practical tutorial on what scroll-driven animations are, when to use them instead of video, when to use them with video, and the specific pattern we settled on for hyperframes.dev.
What "scroll-driven" actually means
Until 2024 or so, "scroll animations" on the web meant one of two things: (1) a JS library reading window.scrollY and updating CSS properties on each frame, or (2) the IntersectionObserver API toggling classes when elements entered or left the viewport. Both worked. Neither was great. The JS approach was expensive and janky; the observer approach was binary, not continuous.
Scroll-driven animations are different. The browser exposes the scroll position itself as a timeline, and you bind a normal CSS animation to that timeline instead of to wall-clock time. The animation progresses as you scroll, in lockstep. No JavaScript. No jank.
The two flavors:
animation-timeline: scroll()— the animation progress is tied to the scroll position of an ancestor scroll container. You scroll, it animates.animation-timeline: view()— the animation progress is tied to where a specific element is within the viewport. As the element enters and crosses the viewport, the animation runs.
The view() timeline is the more useful of the two for most editorial work. It lets you say "when this section is on screen, run this animation as a function of how far through the viewport it has scrolled."
A minimal example
Here is a complete, working scroll-driven animation. Drop this into an HTML file and open it.
<style>
@keyframes reveal {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: reveal linear;
animation-timeline: view();
animation-range: entry 10% cover 30%;
}
</style>
<div style="height: 100vh">scroll down</div>
<div class="card">I appear as I scroll into view.</div>
<div style="height: 100vh">scroll up</div>A few things are doing work here.
The animation-timeline: view() says "tie this animation's progress to where the element is in the viewport." The default range is entry 0% exit 100% — the animation starts when the element first appears at the bottom of the viewport, ends when it has just left at the top.
The animation-range: entry 10% cover 30% is the interesting knob. It says: start the animation when the element is 10% past the start of its entry, and finish it when it has covered 30% of the viewport. This is how you control the feel of a scroll-driven animation. Tighten the range for snappier reveals; loosen it for slower, more cinematic ones.
animation: reveal linear uses linear timing because the scroll is the timing function. The user's scroll velocity is the easing. (You can layer additional easing if you want, but it composes confusingly with scroll velocity. I usually let scroll do the work.)
Scroll vs video: a decision tree
The question I get most: when do I use scroll-driven animations vs a video element? Here is the way I think about it.
Use scroll-driven animations when:
- The motion needs to react to user interaction (scrolling, hovering, dragging).
- The motion is part of the page's layout, not an artifact dropped on top.
- The motion is short — a few seconds of content max.
- The motion is text-heavy and needs to remain selectable/searchable.
Use video when:
- The motion is long (>10 seconds).
- The motion involves complex pixel content (generative backgrounds, compositing, particle systems too expensive to run in CSS).
- The motion needs to be embeddable across platforms (social, email, anywhere that doesn't run your CSS).
- You want pixel-exact playback that doesn't depend on the user's device's rendering quirks.
Use both when:
- You are building a marketing page with a hero animation that needs to look great loading (scroll-driven CSS) but also needs to be embeddable as a video on social (HyperFrames render of the same composition).
That last one is the pattern we use ourselves. The same CSS that drives the scroll-animated hero on the homepage gets rendered, frame-by-frame, into an MP4 for our Twitter and LinkedIn posts. One composition, two outputs.
The hybrid pattern: same CSS, two timelines
Here is the thing that makes this beautiful. The CSS animation does not care what timeline drives it. You can bind the same animation to scroll for the web page and to a video timeline for an MP4 render.
The trick is in how HyperFrames evaluates animations. As I covered in from DOM to MP4, HyperFrames drives animations by manipulating --hf-time and animation delays. For scroll-driven animations specifically, HyperFrames provides a --hf-scroll-progress CSS variable that the composition can use to simulate scroll position deterministically.
A pattern that works:
@keyframes reveal {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: reveal linear forwards;
/* Live web page: drive by scroll. */
animation-timeline: view();
animation-range: entry 10% cover 30%;
/* HyperFrames render: drive by --hf-scroll-progress
(set by the render harness to a synthetic scroll position). */
@media (--hf-render) {
animation-timeline: none;
animation-duration: 1s;
animation-delay: calc(var(--hf-scroll-progress, 0) * -1s);
animation-play-state: paused;
}
}The (--hf-render) media query is a custom feature HyperFrames sets when rendering. In a normal browser, the scroll timeline drives the animation. In the render path, the synthetic time variable drives it. Same animation, two timelines.
A real example: the hyperframes.dev hero
Here is what we actually ship on the homepage, slightly simplified. The hero is a sequence of three "moments" — title, subtitle, demo strip — that scroll into view in sequence.
<section class="hero">
<h1 class="hero-title">HTML in. MP4 out.</h1>
<p class="hero-sub">Deterministic video for the agentic web.</p>
<div class="hero-demo">[demo embed]</div>
</section>
<style>
.hero { min-height: 200vh; padding-top: 20vh; }
.hero-title, .hero-sub, .hero-demo {
animation: rise linear forwards;
animation-timeline: view(block);
animation-fill-mode: both;
}
.hero-title { animation-range: entry 0% entry 40%; }
.hero-sub { animation-range: entry 15% entry 55%; }
.hero-demo { animation-range: entry 30% entry 70%; }
@keyframes rise {
from { opacity: 0; transform: translateY(60px); }
to { opacity: 1; transform: translateY(0); }
}
</style>The animation-range values stagger the elements: the title is already settling as the subtitle starts, which is already in motion as the demo starts. The scroll velocity determines how fast or slow the whole sequence runs. A slow scroll lets the user read each element as it arrives; a fast scroll blows through and shows the final state. Both feel intentional.
For the social version of this hero, we render the same composition through HyperFrames at a fixed pace (the harness drives --hf-scroll-progress from 0 to 1 over 6 seconds). The output is a 6-second MP4 that we post to Twitter. Same CSS. Same animation. Two outputs.
The pitfalls I have hit
A short list of things that have bitten me, that I have not seen documented well.
Pitfall 1: animation-fill-mode: both is essential. Without it, the element snaps back to its initial state when the scroll position is outside the animation range. With it, the element stays at whichever end of the animation is closer.
Pitfall 2: reduced motion is your responsibility. Scroll-driven animations are particularly nauseating for users who prefer reduced motion. Wrap your animations in @media (prefers-reduced-motion: no-preference) or provide a static fallback. Browsers will not do this for you.
Pitfall 3: view() timelines are based on the scroll container's viewport, not the actual visible viewport. If you have nested scroll containers, the timeline reference can surprise you. Use view(block) to be explicit.
Pitfall 4: animation events do not fire on scroll-driven timelines. If you rely on animationend to trigger something, it will not fire when the timeline is scroll. Use IntersectionObserver for completion signals.
What I would build next
Scroll-driven animations in 2026 are the same kind of unlock that CSS Grid was in 2018. They take a thing that was previously hard-and-jank (scroll-tied motion) and make it the default. The interesting question is what new design patterns this opens up.
A few I have been exploring:
- Scroll-driven storytelling — long-scroll pages where the narrative pace is set by the user's scroll. Think Snow Fall but built in 40 lines of CSS instead of a custom JS framework.
- Document-as-video — a hybrid where the same composition is read as a scrollable page on the web and rendered as a linear video for social. We are dogfooding this pattern; it is changing how we structure our docs and blog posts.
- Interactive product demos — scroll-driven walkthroughs that double as embeddable videos for marketing. Build once, distribute everywhere.
If you want to try the hybrid pattern yourself, the playground supports --hf-scroll-progress natively now. Drop in a scroll-driven composition, render it, get an MP4. Same input, two outputs. That is the version of the web I want to live in.
Cite this postBibTeX · APA · Markdown
@misc{park2026scrolldriven,
author = {Ren Park},
title = {Scroll-driven video: turning timelines into scroll positions},
year = {2026},
url = {https://hyperframes.video/blog/scroll-driven-video-timelines},
note = {HyperFrames blog}
}Ren Park. (2026, May 16). Scroll-driven video: turning timelines into scroll positions. HyperFrames. https://hyperframes.video/blog/scroll-driven-video-timelines
[Scroll-driven video: turning timelines into scroll positions](https://hyperframes.video/blog/scroll-driven-video-timelines) — Ren Park, 2026
Ren writes guides, runs workshops, and breaks the CLI on purpose so you do not have to. Previously dev rel at a CI company; before that, an actual filmmaker.
How to animate your logo (without After Effects)
A logo reveal in pure CSS — spring overshoot, wordmark stagger, and a render straight to MP4. No timeline tool, no plugins.
Motion graphics in 80 lines
A complete title sequence — bouncy text, parallax backdrop, signal-color accent, cinematic ease — written in 80 lines of plain HTML. No framework. No tooling beyond the browser.
Stripe-style payment success animation in pure CSS
The animated checkmark, the elastic ring, the confetti — the full Stripe-style payment success animation in CSS. Free source, MP4 export.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.