Image Sequence Scrub
The product-page effect that scrubs a set of frames with scroll progress — the view stays pinned while the imagery plays frame by frame.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/image-sequence-scrub.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ImageSequenceScrub />
Installation
npx shadcn@latest add "https://webberui.com/r/image-sequence-scrub.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/image-sequence-scrub.
Usage
Pass in a set of frame URLs in order; the canvas inside the scroll track container gets pinned and switches frames one by one as the scroll progresses.
import { ImageSequenceScrub } from "@/components/ui/image-sequence-scrub";
const frames = Array.from(
{ length: 120 },
(_, i) => `/sequence/frame-${String(i).padStart(4, "0")}.jpg`
);
<ImageSequenceScrub
frames={frames}
scrollScale={4}
fit="cover"
smooth
alt="Product turntable"
stickyClassName="h-screen"
>
<div className="flex h-full items-end p-10">
<h2 className="text-4xl font-bold text-white">Designed to move.</h2>
</div>
</ImageSequenceScrub>If the effect sits inside a nested scrolling area of the page, pass that scroll container's ref to container and set the stickyClassName height to match the container.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
frames | string[] | — | Frame URLs played in order; scroll progress maps onto the array index |
scrollScale | number | 3 | The scroll track is this multiple of the pinned canvas height; higher values make the scrub finer |
offsetTop | number | 0 | Position where the pinned canvas sticks to the top (px) |
fit | "cover" | "contain" | "cover" | How the image fills the canvas: fill and crop, or fit whole and center |
smooth | boolean | false | Whether to smooth the scrub with a spring (adds momentum but also slight lag) |
posterIndex | number | 0 | Index of the frame used as the static poster under reduced motion |
background | string | — | Fill color for the letterboxing left by contain (a CSS color); transparent by default |
container | RefObject<HTMLElement> | — | Ref of the nested scroll container; the window is the scroll container by default |
alt | string | "Scroll-scrubbed image sequence" | Accessible label for the canvas |
onProgress | (progress, frameIndex) => void | — | Callback fired while scrubbing with the progress (0–1) and the frame index |
onLoadProgress | (loaded, total) => void | — | Callback for frame preloading progress |
stickyClassName | string | "h-screen" | Class for the pinned canvas container; it determines the canvas height |
How it works
- All frames are preloaded as
Imageobjects on mount and drawn onto a single<canvas>, which avoids the flicker of swapping an<img>src frame by frame. - Each scroll only redraws when the frame index actually changes, and repeated requests are coalesced with
requestAnimationFrame; DPR scaling and thecover/containlayout are both computed inside the canvas, and aResizeObservertriggers a redraw whenever the canvas size changes. - The track container's height comes from the measured height of the pinned canvas multiplied by
scrollScale, so the scrub distance is produced correctly whether you scroll the window or a nested container. smoothfeeds scroll progress through a spring, which suits fast flicks; turn it off when you want the frames pinned tightly to the scroll.
Accessibility
- When the user has "reduce motion" enabled at the system level, it renders a single static
posterIndexframe instead and does no scrubbing. - The canvas carries
role="img"and thealtlabel; overlaid content does not take pointer events by default (pointer-events-none), so addpointer-events-autoyourself to interactive elements. - The
Imageload events,requestAnimationFrame, andResizeObserverare all fully cleaned up on unmount.
Sticky Reveal
A sticky text-and-visual reveal: the text on the left flows with the scroll while the visual on the right stays pinned and cross-fades between states.
Zoom Parallax
A photo group that zooms through with the scroll — each image scales from the center at its own rate, creating a fly-through depth parallax.