Chapter navigation
Buttons that jump to a scene and show which one you're in.
Chapters combine two things: knowing which scene the video is in, and scrolling the page to another one. A click scrolls smoothly, so the video scrubs through everything on the way.
Example.tsx
import { useState } from "react";
import { ScrollVideo, useScrollVideo, useScrollVideoUpdate } from "vidscroll";
const CHAPTERS = [
{ title: "Doors", at: 0 },
{ title: "Carriage", at: 4 },
{ title: "Open water", at: 12 },
{ title: "Away", at: 24 },
];
const css = `
.chapters {
position: absolute;
top: 50%;
right: 16px;
display: grid;
gap: 4px;
translate: 0 -50%;
pointer-events: auto;
}
.chapters button[aria-current="true"] {
border-color: #f6b98e;
color: #f6b98e;
}
`;
function ChapterNav() {
const { api } = useScrollVideo();
const [current, setCurrent] = useState(0);
useScrollVideoUpdate(({ time }) => {
let index = 0;
CHAPTERS.forEach((chapter, i) => {
if (time >= chapter.at) index = i;
});
setCurrent(index);
});
return (
<nav className="chapters" aria-label="Chapters">
{CHAPTERS.map((chapter, i) => (
<button key={chapter.title} aria-current={i === current} onClick={() => api?.scrollToTime(chapter.at + 0.5)}>
{chapter.title}
</button>
))}
</nav>
);
}
export default function Example() {
return (
<ScrollVideo src="/train.mp4" length="500vh">
<style>{css}</style>
<ChapterNav />
</ScrollVideo>
);
}The component
ChapterNav.tsx
import { useState } from "react";
import { useScrollVideo, useScrollVideoUpdate } from "vidscroll";
const CHAPTERS = [
{ title: "Doors", at: 0 },
{ title: "Carriage", at: 4 },
{ title: "Open water", at: 12 },
{ title: "Away", at: 24 },
];
export function ChapterNav() {
const { api } = useScrollVideo();
const [current, setCurrent] = useState(0);
useScrollVideoUpdate(({ time }) => {
let index = 0;
CHAPTERS.forEach((chapter, i) => {
if (time >= chapter.at) index = i;
});
setCurrent(index);
});
return (
<nav className="chapters" aria-label="Chapters" style={{ pointerEvents: "auto" }}>
{CHAPTERS.map((chapter, i) => (
<button key={chapter.title} aria-current={i === current} onClick={() => api?.scrollToTime(chapter.at + 0.5)}>
{chapter.title}
</button>
))}
</nav>
);
}Render <ChapterNav /> inside your ScrollVideo: the hooks read from the
nearest one.
How it works
- The current chapter comes from
useScrollVideoUpdate, which runs every frame. CallingsetCurrentwith the same index doesn’t re-render, so React only updates when the chapter actually changes. - Jumping uses
api.scrollToTime. It scrolls the page, smoothly by default; pass{ behavior: "instant" }to jump without scrubbing through. - The half-second offset (
chapter.at + 0.5) lands just inside the chapter, so the right button is highlighted after the jump. pointerEvents: "auto"is needed because the layer over the video ignores clicks by default.
Style the current button with button[aria-current="true"]. The same attribute
tells screen readers which chapter is current.
A menu outside the video
The hooks only work inside the ScrollVideo. For a chapter list elsewhere on
the page, keep the component that calls useScrollVideo inside the video and
lift the api up through your own state or context. With the
element, any script can call
element.scrollToTime() directly.
Related
- Hooks: all three hooks and the api.
- Live values: more per-frame updates through refs.