vidscroll0.3.3

Hooks

Read the video's progress, time and sections from your own components, or control the scroll.

Three hooks connect your components to a scroll video. They work in any component rendered inside ScrollVideo, at any depth.

HookGives youRe-rendersUse it for
useScrollVideoState()Progress, time, frame, active sectionsWhen they changeSmall readouts: a timecode, an active chapter name
useScrollVideoUpdate(fn)The same, every frame, in a callbackNeverAnything animated: write to a ref
useScrollVideo()The api objectWhen the api becomes availableActions: jump to a time, read a section’s progress

All three are used in this example: a timecode, a bar along the bottom, and buttons that jump to a moment.

Scroll inside the preview
Example.tsx
import { useRef } from "react";
import { ScrollVideo, useScrollVideo, useScrollVideoState, useScrollVideoUpdate } from "vidscroll";

function Timecode() {
  const { time, ready } = useScrollVideoState();
  return <p style={{ margin: 0, font: "500 32px monospace" }}>{ready ? time.toFixed(1) : "–"} s</p>;
}

function ProgressBar() {
  const bar = useRef<HTMLDivElement>(null);
  useScrollVideoUpdate(({ linearProgress }) => {
    if (bar.current) bar.current.style.transform = `scaleX(${linearProgress})`;
  });
  return (
    <div
      ref={bar}
      style={{ position: "absolute", left: 0, bottom: 0, width: "100%", height: 4, background: "#f6b98e", transformOrigin: "left" }}
    />
  );
}

function Chapters() {
  const { api } = useScrollVideo();
  return (
    <div style={{ display: "flex", gap: 8, pointerEvents: "auto" }}>
      <button onClick={() => api?.scrollToTime(1)}>Doors</button>
      <button onClick={() => api?.scrollToTime(13)}>Open water</button>
      <button onClick={() => api?.scrollToTime(25)}>Away</button>
    </div>
  );
}

export default function Example() {
  return (
    <ScrollVideo src="/train.mp4" length="400vh">
      <div style={{ position: "absolute", top: 16, left: 16, display: "grid", gap: 12, color: "#fff" }}>
        <Timecode />
        <Chapters />
      </div>
      <ProgressBar />
    </ScrollVideo>
  );
}

useScrollVideoState

Returns the current state and re-renders the component when it changes:

Timecode.tsx
import { useScrollVideoState } from "vidscroll";

export function Timecode() {
  const { ready, time } = useScrollVideoState();
  return <span>{ready ? time.toFixed(1) : "–"} s</span>;
}
FieldTypeMeaning
readybooleanThe video is loaded and following the scroll
progressnumberSmoothed scroll progress through the block, 0 to 1
timenumberVideo time in seconds
framenumberFrame number
activeSectionsstring[]Ids of the sections active right now

While someone scrolls, these values change every frame, so the component re-renders every frame. That’s fine for a few small components. For anything heavier, or anything you animate, use useScrollVideoUpdate.

useScrollVideoUpdate

Calls your function on every frame with the latest state, without re-rendering. Write the result straight to the DOM through a ref:

ProgressBar.tsx
import { useRef } from "react";
import { useScrollVideoUpdate } from "vidscroll";

export function ProgressBar() {
  const bar = useRef<HTMLDivElement>(null);

  useScrollVideoUpdate(({ linearProgress }) => {
    if (bar.current) bar.current.style.transform = `scaleX(${linearProgress})`;
  });

  return <div ref={bar} className="progress-bar" />;
}

The callback receives linearProgress, time, frameIndex, totalFrames, duration and activeSections. It’s also called once right away with the current state. The latest version of your function is always used, so it can read props and state without being recreated.

useScrollVideo

Returns { api }. The api is null until the video component has mounted, so check for it before using it:

Chapters.tsx
import { useScrollVideo } from "vidscroll";

export function Chapters() {
  const { api } = useScrollVideo();
  return (
    <nav style={{ pointerEvents: "auto" }}>
      <button onClick={() => api?.scrollToTime(1)}>Doors</button>
      <button onClick={() => api?.scrollToTime(13)}>Open water</button>
    </nav>
  );
}
MethodWhat it does
scrollToTime(seconds, options?)Scrolls the page to the point where the video shows that time. Smooth by default; pass { behavior: "instant" } to jump. Does nothing until the duration is known.
scrollToProgress(p, options?)Scrolls to a progress between 0 and 1, the same way.
getSectionProgress(id)The same 0 to 1 value as that section’s –progress.
getState()The current state, as passed to useScrollVideoUpdate.
isReady()Whether the video is loaded and following the scroll.
on(event, fn), off(event, fn)Subscribe to events: ready, update, sectionEnter, sectionExit and more.

Smooth scrolling to a time scrubs the video through everything on the way, which reads well for chapter navigation. Buttons placed over the video outside a Section need pointer-events: auto, because the overlay ignores clicks by default.

Outside a ScrollVideo

Used outside a ScrollVideo, the hooks don’t fail: useScrollVideo returns { api: null }, useScrollVideoState returns an idle state with ready: false, and useScrollVideoUpdate never calls your function. They also work inside ScrollFrames.

  • Section: ranges, --progress and active state.
  • ScrollVideo: the component these hooks read from.