Video Scrubbing Alternatives for Scroll-Driven Pages
Scroll-scrubbed video — a clip whose playhead follows the reader’s scroll position — is a frequent request and almost never a good implementation. The reason is structural rather than a matter of optimisation: scroll progress updates on the compositor at display rate, while seeking a video is a main-thread operation limited by the encode’s keyframe interval. The two cannot be made to agree. This guide explains why and covers the three things worth building instead. It sits under scroll-driven media and gallery effects.
When to use this approach
Read this when someone has asked for a scroll-scrubbed video, or when you have one and it stutters. The decision is usually resolved by naming what the video is actually showing:
- A change that could be described in CSS — a product rotating, a diagram assembling, a UI transitioning between two states. This is the most common case and the cheapest answer.
- Footage that genuinely has to be footage — a real recording of something, where no synthetic version would do. This is where an image sequence earns its cost.
- A clip that simply plays — where the scroll connection was never essential, and playing on entry is both simpler and better.
Implementation
1. Establish what the video is showing
If the answer is a synthetic change — an object rotating, a chart building, an interface animating — then a video was already the wrong container for it. A CSS animation of the same change scrubs perfectly, weighs nothing, and stays on the compositor.
.product {
animation: turn linear both;
animation-timeline: view(block);
animation-range: cover 20% cover 80%;
}
@keyframes turn {
from { rotate: y -25deg; }
to { rotate: y 25deg; }
}
2. If it must be footage, ship a sequence
An image sequence is a set of stills, one per frame, swapped according to scroll progress. Because each frame is an independent image, there is no seeking and no keyframe dependency — the cost moves entirely to bandwidth and memory.
/* the frames are stacked and only one is opaque at a time */
.sequence img {
grid-area: 1 / 1;
opacity: 0;
animation: show steps(1) both;
animation-timeline: --sequence;
}
Steps easing rather than linear is what gives a clean frame swap instead of a crossfade between two frames.
Keep sequences short, size them for the largest slot they will occupy rather than the largest screen, and preload them before the section is reached — a sequence that starts downloading when the reader arrives will stutter for exactly the reason the video would have.
3. If the scroll connection is decorative, play on entry
const io = new IntersectionObserver(([entry]) => {
const v = entry.target;
if (entry.isIntersecting) v.play(); else v.pause();
}, { threshold: 0.4 });
document.querySelectorAll('video[data-play-in-view]').forEach((v) => io.observe(v));
This keeps the video, removes the scrubbing, and costs one observer. It is also the only one of the three where the accessibility obligation is significant: a video that starts on its own and runs beyond five seconds needs a pause control under SC 2.2.2.
4. Respect the motion preference in all three
@media (prefers-reduced-motion: reduce) {
.product { animation: none; rotate: none; }
.sequence img:first-child { opacity: 1; }
.sequence img:not(:first-child) { display: none; }
}
For the play-on-entry version, check the preference before calling play() and show the poster frame instead.
Verification
For the CSS version, confirm there are no paint entries during a scroll through the section — a rotation or transform sequence should be entirely compositor work.
For a sequence, measure the transfer size of the section in isolation and compare it against the page’s total budget. Then test on a throttled connection: the failure mode is not stutter but a section that shows a blank frame while the reader is already inside it.
For the play-on-entry version, confirm the video pauses when it leaves the viewport — an unpaused off-screen video continues decoding, which is main-thread work with no visible benefit — and confirm the pause control is reachable by keyboard.
In all three cases, emulate reduced motion and check that the section still communicates what it was showing rather than going blank.
Edge cases and gotchas
Sequence frames decoding late. Swapping to a frame that has not decoded shows nothing. Preload the whole sequence before the section is reachable, or accept a visible gap on slow connections.
Memory from a long sequence. Every decoded frame held in memory is a decoded bitmap, not a compressed file. A 240-frame sequence at full width can occupy far more memory decoded than its transfer size suggests.
steps(1) versus linear. Linear easing between two stacked frames crossfades them, which reads as a soft double image rather than as a frame change. Steps easing is what makes a sequence look like footage.
Autoplaying video and data saving. Browsers and platforms increasingly restrict autoplay, and a play-on-entry implementation must handle a rejected play() promise rather than assuming it succeeded.
Video as a background under text. Whatever the technique, moving footage under text is a contrast and a vestibular problem at once. If the section needs both, dim the media substantially or reconsider the layout.
Browser-specific notes
Seeking behaviour is where the engines differ most, and none of them differ enough to change the conclusion.
Chromium seeks relatively quickly and can decode some formats off the main thread, which makes a scrubbed video look closest to working there — closely enough that the technique is frequently shipped after being tested only in Chromium, and then reported as broken elsewhere.
Safari on iOS restricts inline video playback and seeking more tightly, and low-power mode adds further limits. A scrubbed video that is marginal in Chromium is typically unusable there.
Firefox seeks accurately but not quickly, and its behaviour is the most obviously stepped of the three. If you want a quick demonstration that the technique does not work, Firefox provides it.
Image sequences behave identically everywhere, because they are just images. That consistency is a real part of their appeal.
Frequently Asked Questions
Can requestVideoFrameCallback make scrubbing work?
It improves how you observe frames, not how quickly the video can produce them. The bottleneck is the seek and decode, which the callback does not change. It is genuinely useful for synchronising other page state to the video’s actual displayed frame, which is a different problem.
What about a video with every frame as a keyframe?
That is effectively an image sequence with extra steps: an all-keyframe encode is enormous, and you still pay the main-thread seek. If you have concluded that dense keyframes are the answer, ship the frames as images and get better browser behaviour for the same bytes.
Is there any case where scroll-scrubbed video is acceptable?
A very short clip, at small dimensions, with a low frame rate, where occasional stepping is tolerable — a looping texture rather than a hero narrative. Even then, the same effect built from a short image sequence is usually smaller and always smoother.
How do these compare for accessibility?
The CSS version is the most accessible: it respects the motion preference natively and has no media to control. The image sequence is close behind, since it is a set of images with alt text on the meaningful frame. Play-on-entry video carries the most obligation — pause control, captions, and a check against the motion preference before starting.
How do I convince a stakeholder who has seen this work on another site?
Show them the same site on a mid-range phone, and on Firefox. Scroll-scrubbed implementations that look convincing are almost always image sequences rather than video, and the ones that genuinely are video are the ones that stutter on anything other than a fast desktop in Chromium.
Framing the conversation around the underlying content also helps: once the effect is described as “the product rotates as you scroll” rather than “the video scrubs”, the CSS version is an obvious improvement rather than a compromise.
Related
- Ken Burns pan on scroll — the ambient media effect that does work well from a timeline
- IntersectionObserver fallback for scroll animations — the observer pattern used for play on entry
- Pause, Stop, Hide for scroll animations — the control an autoplaying clip requires
- Core Web Vitals for Scroll & View Transitions — how a heavy sequence affects loading metrics