Transcript reel
FreeA transcript on a curved surface. The line being read is large and faces you; the lines before and after bend away along a cylinder, so you keep your place while it plays. Every line shows its time and speaker, and clicking one seeks to it.
pnpm dlx shadcn@latest add @caliper/transcript-reelWhen to use #
Use it for
- Meeting notes, interviews and call recordings that play alongside the text.
- Anywhere the current line matters more than the rest but context should stay in view.
Skip it when
- A transcript people search or copy from; use a plain list with find.
- Very long recordings with no way to jump; pair it with chapters.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Playing is a button, never automatic
- Play advances one line at a time and stops at the end. Nothing announces each line to a screen reader while it plays.
- Reduced motion makes it a list
- With reduced motion the reel is flat: a plain scrolling list with the current line highlighted and kept in the middle.
- The wheel hands the page back
- Scrolling over the reel steps through the lines, and at the first or last line the page scrolls on instead of being trapped.
Styling
- Only the current line is large
- The surface turns 26 degrees a line; the neighbours stay legible and the far ones fade. A long line grows the current line and is clipped on the others.
Installation #
pnpm dlx shadcn@latest add @caliper/transcript-reel
Adds its 7 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { TranscriptReel, type TranscriptLine } from "@/components/ui/transcript-reel" const talk: TranscriptLine[] = [ { time: 0, speaker: "Mira", text: "Thanks for joining. Today is about the depth set." }, { time: 4, speaker: "Jo", text: "Ten parts, six free and four pro." }, { time: 8, speaker: "Mira", text: "Each one uses the third axis for a reason." }, { time: 12, speaker: "Sam", text: "Like the skyline chart: height is value." }, { time: 16, speaker: "Jo", text: "And flat it is still an exact heatmap." }, { time: 20, speaker: "Mira", text: "Right. Nothing depends on seeing the 3D." }, { time: 24, speaker: "Sam", text: "What about reduced motion?" }, { time: 28, speaker: "Mira", text: "Everything lands in its final pose at once." }, { time: 32, speaker: "Jo", text: "Keyboard paths for all of them?" }, { time: 36, speaker: "Mira", text: "Yes, and a flat twin for every scene." }, { time: 40, speaker: "Sam", text: "Then let us ship it." }, { time: 44, speaker: "Jo", text: "I will write the changelog." }, ]; export function TranscriptReelDemo() { const [line, setLine] = useState(3); return ( <div className="w-full max-w-xl"> <TranscriptReel label="Depth set review, 12 lines" lines={talk} current={line} onCurrentChange={setLine} interval={2200} /> </div> ); }
API reference #
TranscriptReel
component
Accessibility #
It exposes group semantics, with aria-current, aria-roledescription, aria-label and aria-live kept in step with state. Changes that matter are announced through a live region.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.