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.
On this page
Installation
Usage
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.
States
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
| Field | Type | Notes |
|---|---|---|
id | string | Stable per utterance |
role | "user" | "assistant" | "system" | System lines render as quiet dividers |
speaker | string | Overrides the role label, for example a name |
text | string | Settled words |
interim | string | Unsettled words, appended in a lighter gray |
at | number | Ms from the start of the session, shown as m:ss |
channel | "voice" | "text" | Typed turns get a keyboard glyph |
interrupted, unspoken | boolean, string | The cut and the words never spoken |
live | boolean | Still streaming: shows the streaming caret |
Props
| Prop | Type | Notes |
|---|---|---|
segments | TranscriptSegment[] | In order |
userLabel, assistantLabel | string | Default You and Assistant |
follow | boolean | Stick to the bottom while at the bottom. Default true |
onSeek | (ms) => void | Timestamps become seek buttons |
activeId | string | The segment being played back |
showTimestamps | boolean | Default true |
layout | "stacked" | "inline" | Live conversation, or a dense meeting log |
empty | ReactNode | Replaces the empty state |
label | string | Accessible name. Default Transcript |
announce | boolean | Read finished replies once. Default true |
Keyboard
| Key | Action |
|---|---|
Tab | Focus the log, then each timestamp when seeking is on |
Arrow keys, Page Up, Page Down | Scroll the focused log. Scrolling up releases follow |
Enter, Space on a timestamp | Seek to that moment |
Accessibility
- The transcript is a focusable
logwith 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
sectionnamed 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.