Live values
Numbers, clocks and SVG drawings that follow the scroll, updated every frame without re-rendering.
Some effects can’t be written in CSS: a clock that counts minutes, a number
that climbs, an SVG ring that fills. For those, useScrollVideoUpdate gives
you the state on every frame, and you write the result straight to the page
through a ref.
import { useRef } from "react";
import { ScrollVideo, useScrollVideoUpdate } from "vidscroll";
function Clock() {
const ref = useRef<HTMLParagraphElement>(null);
useScrollVideoUpdate(({ linearProgress }) => {
const minutes = 7 * 60 + 40 + linearProgress * 35;
const text = `${Math.floor(minutes / 60)}:${String(Math.floor(minutes % 60)).padStart(2, "0")}`;
if (ref.current && ref.current.textContent !== text) ref.current.textContent = text;
});
return <p ref={ref} style={{ margin: 0, color: "#fff", font: "500 40px monospace" }}>7:40</p>;
}
function Ring() {
const ref = useRef<SVGCircleElement>(null);
useScrollVideoUpdate(({ linearProgress }) => {
ref.current?.setAttribute("stroke-dashoffset", String(1 - linearProgress));
});
return (
<svg viewBox="0 0 100 100" width="88" height="88" aria-hidden="true">
<circle cx="50" cy="50" r="44" fill="none" stroke="rgb(255 255 255 / 0.25)" strokeWidth="6" />
<circle
ref={ref}
cx="50"
cy="50"
r="44"
fill="none"
stroke="#f6b98e"
strokeWidth="6"
pathLength={1}
strokeDasharray="1"
strokeDashoffset="1"
transform="rotate(-90 50 50)"
/>
</svg>
);
}
export default function Example() {
return (
<ScrollVideo src="/train.mp4" length="400vh">
<div style={{ position: "absolute", top: 16, left: 16, display: "flex", alignItems: "center", gap: 16 }}>
<Ring />
<Clock />
</div>
</ScrollVideo>
);
}A clock
import { useRef } from "react";
import { useScrollVideoUpdate } from "vidscroll";
export function Clock() {
const ref = useRef<HTMLParagraphElement>(null);
useScrollVideoUpdate(({ linearProgress }) => {
const minutes = 7 * 60 + 40 + linearProgress * 35;
const text = `${Math.floor(minutes / 60)}:${String(Math.floor(minutes % 60)).padStart(2, "0")}`;
if (ref.current && ref.current.textContent !== text) ref.current.textContent = text;
});
return <p ref={ref}>7:40</p>;
}The clock runs from 7:40 to 8:15 across the video. Writing only when the text changes keeps the work small.
An SVG ring
pathLength={1} makes the circle’s length 1, so the dash offset is simply
1 - progress:
import { useRef } from "react";
import { useScrollVideoUpdate } from "vidscroll";
export function Ring() {
const ref = useRef<SVGCircleElement>(null);
useScrollVideoUpdate(({ linearProgress }) => {
ref.current?.setAttribute("stroke-dashoffset", String(1 - linearProgress));
});
return (
<svg viewBox="0 0 100 100" width="88" height="88">
<circle
ref={ref}
cx="50"
cy="50"
r="44"
fill="none"
stroke="currentColor"
strokeWidth="6"
pathLength={1}
strokeDasharray="1"
strokeDashoffset="1"
transform="rotate(-90 50 50)"
/>
</svg>
);
}Why a ref instead of state
The callback runs on every frame while someone scrolls, about 60 times a second. Setting React state that often re-renders the component every frame, which costs more than the change itself. Writing to the DOM through a ref skips React entirely.
useScrollVideoState is fine for values that change rarely, like the name of
the current chapter. For anything continuous, use useScrollVideoUpdate with a
ref.
What the callback receives
| Field | Meaning |
|---|---|
linearProgress | Smoothed scroll progress, 0 to 1 |
time | Video time in seconds |
frameIndex, totalFrames | Current frame and frame count |
duration | Video length in seconds |
activeSections | Ids of the active sections |
Use linearProgress for things tied to the scroll, and time for things tied
to the footage.
Related
- Hooks: all three hooks.
- Scroll-driven CSS: effects that need no JavaScript.