Dependency-free · TypeScript · Native scroll

Make every scroll
a moment in time.

A tiny, framework-agnostic utility that maps vertical scroll progress to video frames—smoothly, reversibly, and without hijacking the page.

Try the demo

Live demo

Keep scrolling. The timeline is yours.

Scroll down to move through the film. Scroll back up and time moves backward with you. No autoplay loop, no scroll-jacking.

00%

Scroll to move through time

At first, the browser is simply paused.

Separation of concerns

CSS builds the stage.
JavaScript controls time.

01 / CSS

The scroll runway

A tall outer section gives the page room to scroll. A sticky inner stage holds the composition in view. Your CSS still owns layout, overlays, breakpoints, and visual progress.

.story { height: 400vh; }
.stage {
  position: sticky;
  top: 0;
  height: 100svh;
}

02 / JavaScript

The time mapping

The utility measures the runway, normalizes its travel to 0–1, and seeks the paused video to the matching time. Work is coalesced to animation frames.

progress = clamp(scrolled / runway)
time = progress × video.duration
video.currentTime = time

03 / Video

The frame delivery

The browser decoder supplies the frame. Short GOP encoding and frequent keyframes make seeking feel immediate; a long GOP makes even excellent JavaScript feel sluggish.

Three small pieces

From install to first frame.

Use the baseline stylesheet or bring your own layout entirely.

1

Install from GitHub

The package is public, but is not represented as npm-published.

npm install github:arvindang/scroll-video-scrubber
2

Add a runway, a sticky stage, and a video

<section class="story" data-video-scrubber>
  <div class="stage" data-svs-sticky>
    <video data-svs-video muted playsinline>
      <source src="story.mp4" type="video/mp4" />
    </video>
    <div data-svs-progress></div>
  </div>
</section>
3

Create the controller

import { createVideoScrubber } from
  "@arvindang/scroll-video-scrubber";
import "@arvindang/scroll-video-scrubber/styles.css";

const scrubber = createVideoScrubber({
  root: "[data-video-scrubber]",
});

For static pages, import autoInitVideoScrubbers and call it once to enhance every [data-video-scrubber] root. Per-root data-svs-* attributes can override selectors, frame rate, and reduced-motion behavior.

Small, explicit API

Use selectors or elements.

Sensible discovery handles the basic case. Options are there when your component needs tighter control.

OptionDefaultPurpose
rootrequiredThe outer scroll runway.
videodiscoveredVideo element or selector.
stickydiscoveredSticky stage used for geometry.
progressdiscoveredTarget for --svs-progress.
enabledtrueStarts the instance enabled or disabled.
frameRate30Skips sub-frame seek noise.
respectReducedMotiontruePreserves the accessible fallback.
reducedMotionQuerysystem preferenceCustom motion media query.
unlockRootMargin200px 0pxControls how early media unlock monitoring begins.
seekRecoverytrueRetries a buffered, stalled URL-backed film once. Disable for recovery managed by another player.
onReady—Runs after metadata is usable.
onProgress—Reports normalized progress.
onError—Reports setup or media errors.

Production checklist

The asset is part of the interaction.

Encode a short GOP

Start with a keyframe every 0.25–0.5 seconds, then test the real device mix.

Serve byte ranges

Fast seeking depends on correct media MIME types and HTTP range support.

Repeat the meaning

Keep essential copy outside the video and caption meaningful audio.

Honor reduced motion

The default fallback returns control to an ordinary, user-operated video.

Browser reality

Mobile Safari may require a muted play/pause unlock before reliable seeking. The utility waits for frame data before applying queued scroll progress. It can reload a visible film once if a buffered seek stays stuck for 2.5 seconds; custom streams keep their own recovery. Returning to a tab also resynchronizes the film. You should still test your final encoding on Safari/iOS, Chrome/Android, and Firefox.

MIT licensed

Native scroll.
Your video. Your story.

View the source on GitHub