Skip to content
VoiceTranscriptMenu

Transcript

Live voice transcript with speaker labels, unsettled words in a lighter gray, timestamps that seek, typed turns and a clean cut where the user interrupted.

Interim words settle, the reply streams, the user cuts in, then types.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/transcript.json

Usage

tsx
import { Transcript } from "@/components/ui/chatcn/transcript"

<div className="flex h-96 flex-col">
  <Transcript
    segments={[
      { id: "1", role: "user", at: 3000, text: "Why did the deploy fail?" },
      { id: "2", role: "assistant", at: 5000, text: "The type check broke on", live: true },
    ]}
  />
</div>

Give it a parent with a height. It fills it and scrolls inside. Each segment is one utterance: update interim while the recognizer is still guessing, then move the words into text once they settle.

Examples

Playback

Pass onSeek and every timestamp becomes a button. Pass activeId and the line being played is highlighted and kept in view. The inline layout puts time and speaker in a gutter, for meeting capture.

A finished call: the active line follows the playhead and every timestamp seeks.

States

Empty, a spoken and typed mix with mode switches, and long content.

Barge-in

When the user talks over the reply, set interrupted: true with what was said in text and the rest in unspoken. The transcript draws a hairline at the exact cut, strikes the words that were never spoken and labels it Interrupted, so the record matches what the user heard.

Segment

FieldTypeNotes
idstringStable per utterance
role"user" | "assistant" | "system"System lines render as quiet dividers
speakerstringOverrides the role label, for example a name
textstringSettled words
interimstringUnsettled words, appended in a lighter gray
atnumberMs from the start of the session, shown as m:ss
channel"voice" | "text"Typed turns get a keyboard glyph
interrupted, unspokenboolean, stringThe cut and the words never spoken
livebooleanStill streaming: shows the streaming caret

Props

PropTypeNotes
segmentsTranscriptSegment[]In order
userLabel, assistantLabelstringDefault You and Assistant
followbooleanStick to the bottom while at the bottom. Default true
onSeek(ms) => voidTimestamps become seek buttons
activeIdstringThe segment being played back
showTimestampsbooleanDefault true
layout"stacked" | "inline"Live conversation, or a dense meeting log
emptyReactNodeReplaces the empty state
labelstringAccessible name. Default Transcript
announcebooleanRead finished replies once. Default true

Keyboard

KeyAction
TabFocus the log, then each timestamp when seeking is on
Arrow keys, Page Up, Page DownScroll the focused log. Scrolling up releases follow
Enter, Space on a timestampSeek to that moment

Accessibility

  • The transcript is a focusable log with its own name. It stays silent while words stream; finished assistant replies are read once through a polite live region.
  • Each speaker group is a section named after the speaker. Timestamp buttons say what they do: Play from 12 seconds, You.
  • Typed turns and unspoken words carry screen reader labels (Typed, Not spoken), not just a glyph or a strike.
  • The playing line has aria-current, so its highlight is not color alone.

Motion

  • Words append with no per-word effect. Interim words change gray as they settle, which never shifts the layout.
  • Follow is a critically damped chase from the shared stick-to-bottom hook, instant under reduced motion. Any upward scroll releases it at once.
  • The playback highlight fades its background over 150 ms. Keeping the active line in view scrolls smoothly, or instantly under reduced motion.