Scroll Effects

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

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/image-sequence-scrub.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
Active edited usageDefault props
<ImageSequenceScrub/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/image-sequence-scrub.en.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

PropTypeDefaultDescription
framesstring[]—Frame URLs played in order; scroll progress maps onto the array index
scrollScalenumber3The scroll track is this multiple of the pinned canvas height; higher values make the scrub finer
offsetTopnumber0Position 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
smoothbooleanfalseWhether to smooth the scrub with a spring (adds momentum but also slight lag)
posterIndexnumber0Index of the frame used as the static poster under reduced motion
backgroundstring—Fill color for the letterboxing left by contain (a CSS color); transparent by default
containerRefObject<HTMLElement>—Ref of the nested scroll container; the window is the scroll container by default
altstring"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
stickyClassNamestring"h-screen"Class for the pinned canvas container; it determines the canvas height

How it works

  • All frames are preloaded as Image objects 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 the cover/contain layout are both computed inside the canvas, and a ResizeObserver triggers 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.
  • smooth feeds 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 posterIndex frame instead and does no scrubbing.
  • The canvas carries role="img" and the alt label; overlaid content does not take pointer events by default (pointer-events-none), so add pointer-events-auto yourself to interactive elements.
  • The Image load events, requestAnimationFrame, and ResizeObserver are all fully cleaned up on unmount.