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.
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 vidscrollpnpm add vidscrollyarn add vidscrollbun add vidscrollGive it a video file and, optionally, how much scrolling the whole video should take. Everything inside is drawn over the video.
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).
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:
- Progress. How far you are through the block, from 0 to 1.
- Smoothing. Progress follows the scroll with a short delay, so small scroll steps glide instead of jumping.
- Easing. An optional curve that reshapes progress, so the video can start slowly or settle at the end.
- 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.
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.
postershows an image while loading. By default the video’s first frame is used.loaderreplaces the loading overlay:false, a React node, or a function of the loading state.onLoadreports how the video ended up being served ("original","optimized","cache"or"stream"), andonErrorreceives errors such as a wrong path or a YouTube link.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | required | Video file URL. A file, not a page: YouTube and similar links are rejected. |
length | string | number | "auto" | "400vh", "2000px" or pixels. "auto" is 40vh per second of video. |
easing | EasingName | (t) => number | "none" | Curve from scroll progress to video time. |
smoothScroll | boolean | SmoothScrollOptions | false | Eased mouse-wheel scrolling for the page. Options: tau (85), wheelMultiplier (1). |
smoothingTauMs | number | 100 | How 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. |
optimize | boolean | OptimizeOptions | true | Re-encode slow-to-seek videos in the browser. Options: maxKeyframeGap (0.5 s), maxResolution (720), maxFps (30), cache (true), wait (false). |
fullPreload | boolean | true | false streams the file directly and skips optimization. |
poster | string | false | first frame | Image shown while loading, or false for none. |
loader | ReactNode | false | (state, builtIn) => ReactNode | built-in | Replaces the loading overlay and the corner badge. |
onLoad | ({ source, probe }) => void | none | Called once the final video is known. |
onError | (error: Error) => void | none | Called when the video can't be loaded or played. |
fps | number | 30 | Frame rate for frame-based section ranges. Replaced by the measured rate when the browser can measure it. |
warmup | boolean | number | true | Touch the whole timeline once while loading. false or a number of steps. |
debug | boolean | false | Log engine internals to the console. onDebug receives them. |
className, style | string, CSSProperties | none | Applied 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.
Related
- How it works: the full chain from scroll to frame, and loading.
- Section: ranges, active state and the
--progressvariable. - Hooks: read progress, time and active sections from your own components.
- Preparing videos: why keyframes matter and how to pre-encode with the CLI.