caliper uidocs

Transcript reel

Free

A 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.

Installs components/ui/transcript-reel.tsxDepends on @radix-ui/react-slot, lucide-reactMotion honours reduced motion
pnpm dlx shadcn@latest add @caliper/transcript-reel
Line 4 of 12

When 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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
labelstring"Transcript"
lines *readonly TranscriptLine[]—
currentnumber—Index of the current line. Controlled when set.
defaultCurrentnumber0
onCurrentChange((index: number, line: TranscriptLine) => void)—Called when a click, a key, the wheel, a button or playback moves to another line.
playingboolean—Controlled when set.
onPlayingChange((playing: boolean) => void)—
intervalnumber2600Milliseconds each line stays current while playing.
statusDepthStatus"ready"
messagesDepthMessages—
onRetry(() => void)—
disabledboolean—
classNamestring—

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.

KeyBehaviour
SpaceActivates the focused control.
↓Moves to the next item or value.
↑Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.

Motion #

It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.