vidscroll0.3.3

ScrollVideo

Pins a video to the screen and moves through it frame by frame as the page scrolls.

<ScrollVideo> takes up a block of the page. While you scroll through that block, the video stays pinned to the screen and its position follows the scroll: halfway through the block is halfway through the video. Content inside it, usually sections, sits on top of the video.

Scroll inside the preview
Example.tsx
import { ScrollVideo, Section } from "vidscroll";

export default function Example() {
  return (
    <ScrollVideo src="/train.mp4" length="400vh">
      <Section id="doors" start={0} end={0.3}>
        <h2>Doors close</h2>
      </Section>
      <Section id="water" start={0.55} end={0.9}>
        <h2>Open water</h2>
      </Section>
    </ScrollVideo>
  );
}

To try the props on a live video and copy the result, open the playground.

Basic usage

npm install vidscroll

Give it a video file and, optionally, how much scrolling the whole video should take. Everything inside is drawn over the video.

Hero.tsx
import { ScrollVideo } from "vidscroll";

export function Hero() {
  return <ScrollVideo src="/videos/hero.mp4" length="400vh" />;
}

The block is as tall as length plus one screen. The video is pinned for the whole length, then the page carries on below it. A page can hold any number of scroll videos; each one only plays while you pass through it.

Sections

Wrap content in <Section> to show it over part of the video. A section is active while the scroll is inside its range and fades in and out by default. Ranges can be given in scroll progress (start, end), seconds (fromTime, toTime) or frames (fromFrame, toFrame).

Hero.tsx
import { ScrollVideo, Section } from "vidscroll";

export function Hero() {
  return (
    <ScrollVideo src="/videos/hero.mp4" length="400vh">
      <Section start={0} end={0.3}>
        <h2>Doors close</h2>
      </Section>
      <Section fromTime={12} toTime={18}>
        <h2>Open water</h2>
      </Section>
    </ScrollVideo>
  );
}

How scrolling maps to the video

Every frame, vidscroll turns the scroll position into a point in the video:

  1. Progress. How far you are through the block, from 0 to 1.
  2. Smoothing. Progress follows the scroll with a short delay, so small scroll steps glide instead of jumping.
  3. Easing. An optional curve that reshapes progress, so the video can start slowly or settle at the end.
  4. Video time. The eased value times the video’s duration.

Three props control this: length sets how much scrolling covers the video, easing sets the curve, and smoothScroll with smoothingTauMs set how the motion feels.

length

How much scrolling plays the whole video. Accepts "400vh", "2000px" or a number of pixels. vh is the viewport height, so "400vh" is four screens of scrolling whatever the device. length never changes the video itself: in the options example below, the readout’s video time stays the same while the scroll total grows or shrinks.

The default, "auto", gives 40vh per second of video, with at least one screen. Set it explicitly when content follows the video: with "auto", the block only knows its height once the video’s duration has loaded, so anything below it moves down at that point.

easing

A preset name or your own function from 0–1 to 0–1. With "none", scrolling moves through the video at a constant rate. Try the options here; the code underneath updates as you change them.

easing
length
fit
Scroll inside the preview
Example.tsx

Presets: none, inQuad, outQuad, inOutQuad, inCubic, outCubic, inOutCubic, inOutSine. A custom function must go from 0 to 1 and never go backwards; time-based section ranges and scrollToTime rely on that.

Smoothing

smoothScroll eases mouse-wheel scrolling for the whole page, which removes the steps a wheel makes. Touch and keyboard scrolling are left alone, and it turns itself off when the visitor prefers reduced motion.

smoothingTauMs sets how closely the video follows the scroll: 100 ms by default, 35 ms when smoothScroll is on (the page scroll is already smooth). After fast scrolling the follow loosens briefly, so the video glides to a stop instead of jumping.

Loading and optimization

Most exported videos have keyframes several seconds apart, which makes every jump to a new position slow. When optimize is on (the default), vidscroll downloads the file, checks its keyframes and, if they’re too far apart, re-encodes it in the browser and caches the result. You can scroll the original while that runs; a small badge shows the progress.

To skip that work, prepare the file ahead of time with npx vidscroll encode hero.mp4. Then it loads and scrubs smoothly right away.

  • poster shows an image while loading. By default the video’s first frame is used.
  • loader replaces the loading overlay: false, a React node, or a function of the loading state.
  • onLoad reports how the video ended up being served ("original", "optimized", "cache" or "stream"), and onError receives errors such as a wrong path or a YouTube link.

Props

PropTypeDefaultDescription
srcstringrequiredVideo file URL. A file, not a page: YouTube and similar links are rejected.
lengthstring | number"auto""400vh", "2000px" or pixels. "auto" is 40vh per second of video.
easingEasingName | (t) => number"none"Curve from scroll progress to video time.
smoothScrollboolean | SmoothScrollOptionsfalseEased mouse-wheel scrolling for the page. Options: tau (85), wheelMultiplier (1).
smoothingTauMsnumber100How closely the video follows the scroll, in ms. 35 when smoothScroll is on.
fit"cover" | "contain""cover"cover fills the screen and crops; contain shows the whole frame.
optimizeboolean | OptimizeOptionstrueRe-encode slow-to-seek videos in the browser. Options: maxKeyframeGap (0.5 s), maxResolution (720), maxFps (30), cache (true), wait (false).
fullPreloadbooleantruefalse streams the file directly and skips optimization.
posterstring | falsefirst frameImage shown while loading, or false for none.
loaderReactNode | false | (state, builtIn) => ReactNodebuilt-inReplaces the loading overlay and the corner badge.
onLoad({ source, probe }) => voidnoneCalled once the final video is known.
onError(error: Error) => voidnoneCalled when the video can't be loaded or played.
fpsnumber30Frame rate for frame-based section ranges. Replaced by the measured rate when the browser can measure it.
warmupboolean | numbertrueTouch the whole timeline once while loading. false or a number of steps.
debugbooleanfalseLog engine internals to the console. onDebug receives them.
className, stylestring, CSSPropertiesnoneApplied to the outer block.

easing and smoothingTauMs apply immediately when they change. A new length or fps rebuilds the scroll mapping; a new src, fullPreload or optimize reloads the video. warmup and debug are read when the video starts.

  • How it works: the full chain from scroll to frame, and loading.
  • Section: ranges, active state and the --progress variable.
  • Hooks: read progress, time and active sections from your own components.
  • Preparing videos: why keyframes matter and how to pre-encode with the CLI.