Headless / Transcription
Live transcript
Stream speech into a transcript with partial results, speakers and connection states.
17 public parts · 2examples
@slices.sh/transcription/headlessView as Markdown ↗Your design. Our behavior.
Explore the tree to see what each part controls. Gates reveal the relevant state; roots outline their scope.
Transcript.Live · Scope
Installation
Choose what your component needs. Run these commands in an initialized Next.js app.
Before you start
The steps below run in a Next.js app with shadcn set up (it has a components.json). In an existing Next.js app, such as one from create-next-app, set shadcn up first:
npx shadcn@latest init -t next -b base -p novaOr start fresh: the slices CLI creates the app and sets it up.
npx slices.sh installInstall the service registry
Adds services/index.ts, which import "@/services" loads. A provider setup writes the same file, so skip this if your app has one.
npx add servicesInstall the headless library
npmAdd the React parts to your app, then compose them with your own elements.
npm install @slices.sh/transcription@^0.1.11Import from @slices.sh/transcription/headless.
Install the service
Connect the parts to a provider. Skip this step if this service is already configured in your app.
npx add transcription.browserAdds service configuration and API routes to your app, with the provider dependencies. For a headless composition, add import "@/services" after "use client".
The slices CLI installs the service and provisions its required resources in one command.
Provider setup notes
- There is no route to install: this provider has no server half. The shared route app/api/transcription/[config]/[[...path]]/route.ts comes with the first provider that has one (deepgram, gladia, openai).
- Firefox has no speech recogniser: the config then grants nothing and the microphone parts do not render.
Start small
Parts bring the behavior. Use their default elements, or supply your own with render.
"use client"import "@/services"import { Transcript } from "@slices.sh/transcription/headless"export function Example() { return ( <Transcript.Live enabled="live"> <Transcript.LiveToggle> {({ listening }) => (listening ? "Stop" : "Start listening")} </Transcript.LiveToggle> <Transcript.LiveIdle>Press Start to begin.</Transcript.LiveIdle> <Transcript.Log> <Transcript.Segments> <Transcript.Segment> <Transcript.Time /> <Transcript.Speaker /> <Transcript.Text /> </Transcript.Segment> </Transcript.Segments> <Transcript.Partial /> </Transcript.Log> <Transcript.Reconnecting>Reconnecting…</Transcript.Reconnecting> <Transcript.LiveFailed> {({ message }) => <p>{message}</p>} </Transcript.LiveFailed> <Transcript.Export format="txt">Save transcript</Transcript.Export> </Transcript.Live> )}Select a live provider such as browser, Deepgram or Gladia. Speakers and timecodes only appear when the provider supplies them.
API reference
Every public part used by this experience is listed below. Gates render children without adding an element. Element parts accept native element props and render; function children receive the state declared in their props. Scopes provide context rather than a visual element.
Transcript.Livescope
Live is a root over a microphone transcribed as somebody speaks. LiveToggle starts or finishes, and renders nothing where the config cannot listen in this browser. Listen when={…} device={…} renders nothing and listens while when holds, for a transcriber something else switches, such as a call's captions. LiveStatus is the always-mounted announcer. LiveIdle invites before anything is heard, LiveFailed gives { error, message } when the session dropped for good. Partial is the hypothesis row at the end, hidden from assistive technology. Heard hands { text, final } for passing words on, to Room.PublishCaption for example. Reconnecting, MicDenied and Devices mean what they mean for Dictation.
Capability: live
Props
6 documented props
| Prop | Type | Default |
|---|---|---|
config | string | — |
enabled | Enabled | — |
languages | string[] | — |
keywords | string[] | — |
maxDurationMs | number | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-root.tsx
Transcript.LiveToggleaction · <button>
Starts listening, or finishes. One control, as a microphone always is. Absent where the instance cannot listen at all — Firefox, on the browser's recogniser — rather than a button that did nothing when pressed and said nothing about why. → docs/rules/capability-honesty.md
Capability: live
Required scope
Transcript.LiveToggle must be inside <Transcript.Live>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { listening: boolean }) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-toggle.tsx
Transcript.Listenscope
Live is a root over a microphone transcribed as somebody speaks. LiveToggle starts or finishes, and renders nothing where the config cannot listen in this browser. Listen when={…} device={…} renders nothing and listens while when holds, for a transcriber something else switches, such as a call's captions. LiveStatus is the always-mounted announcer. LiveIdle invites before anything is heard, LiveFailed gives { error, message } when the session dropped for good. Partial is the hypothesis row at the end, hidden from assistive technology. Heard hands { text, final } for passing words on, to Room.PublishCaption for example. Reconnecting, MicDenied and Devices mean what they mean for Dictation.
Capability: live
Required scope
Transcript.Listen must be inside <Transcript.Live>.
Props
3 documented props
| Prop | Type | Default |
|---|---|---|
whenRequired | boolean | — |
deviceThe microphone to hear, when something else already chose one; | string | null | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/listen.tsx
Transcript.LiveStatuscontent · <p>
What a live transcript tells a screen reader. Always mounted, because a live region that appears with its content already in it announces nothing. It carries the state and how many lines have settled rather than the lines themselves: the log below reads those out as they arrive, and repeating them here would say everything twice. → components/headless/transcription/transcript-log.tsx
Capability: live
Required scope
Transcript.LiveStatus must be inside <Transcript.Live>.
Props
2 documented props · Native <p> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: Announced) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-status.tsx
Transcript.LiveIdlegate
Nothing has been heard yet. Where the invitation goes. A live transcript with no gate here drew an empty bordered box the first time anybody opened it, which reads as broken rather than as ready — and it is the state every visitor sees first. → docs/rules/states-gate-themselves.md
Capability: live
Required scope
Transcript.LiveIdle must be inside <Transcript.Live>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-idle.tsx
Transcript.LiveFailedgate
The session dropped for good, with the reason. `LiveState` has had a `failed` case from the beginning and nothing rendered it, so a stream that died left the last few lines on screen and no explanation at all — which reads as a transcript that has simply stopped being interesting. `message` is the service's own words for why, `null` where it had none.
Capability: live
Required scope
Transcript.LiveFailed must be inside <Transcript.Live>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { error: TranscriptionErrorSlug; message: string | null }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-failed.tsx
Transcript.Partialcontent · <span>
The hypothesis at the end of a live transcript. Apart from the segments because it is the one part that moves: a list that mixed them would re-key and re-animate the whole transcript on every guess. It is the whole row rather than something inside one, so a transcript with nothing pending is a list of exactly the lines that were said — an empty row left standing for the next guess is one a screen reader counts and moves through. And hidden from assistive technology, because it sits inside the live region the settled lines arrive in: a guess appearing there is an addition like any other, and a half-word read out and then read again as part of the finished line is the machine talking over the room. → components/headless/transcription/transcript-log.tsx
Capability: live
Required scope
Transcript.Partial must be inside <Transcript.Live>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/live-partial.tsx
Transcript.Heardgate
What was just heard: the hypothesis while it grows, then the line it settled into. For handing a live transcript on — to captions in a call, say — rather than for drawing it, which the segments and the partial already do. Nothing while listening has heard nothing.
Capability: live
Required scope
Transcript.Heard must be inside <Transcript.Live>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { text: string; final: boolean }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/heard.tsx
Transcript.Logcontent · <ul>
The scrolling list a live transcript arrives in. It stays at the newest line while it is already there, and stops the moment somebody scrolls up — sticking to the bottom unconditionally is the version everybody writes first and nobody can use, because reading what was said a minute ago gets you yanked back down by the next sentence. `Transcript.Latest` is the way back. A live region on the list itself, and deliberately not `role="log"`. What `log` adds is exactly the two attributes below — and what it takes away is the list: a role replaces the element's own, so the rows underneath stop being items of anything and a screen reader can no longer say how many there are or move between them. → hooks/transcription/use-follow.ts
Required scope
Transcript.Log must be inside <Transcript.Live>.
Props
2 documented props · Native <ul> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { following: boolean }) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/transcript-log.tsx
Transcript.Latestaction · <button>
Back to the newest line. Rendered only while the list is not already there, because that is the only time it means anything — and because a button that is always present beside a list that is always at the bottom teaches people to ignore it.
Required scope
Transcript.Latest must be inside <Transcript.Live>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/transcript-latest.tsx
Transcript.Reconnectinggate
The recogniser dropped and is coming back. Its own gate because it is not a failure and not idleness: Chrome's recogniser ends itself after a silence even when it was told to be continuous, and a screen with only "listening" to show left somebody talking into a socket that had closed. The contract has modelled `reconnecting` from the start and nothing was reading it. → services/transcription/ports.ts
Capability: live
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/reconnecting.tsx
Transcript.MicDeniedgate
The browser will not give this page a microphone. Three refusals rather than one, because only one of them is a setting: no device, an insecure page or a browser without `getUserMedia` is nobody's to grant; a refusal can be taken back in the address bar; and a microphone another application is holding needs nothing changed at all, only the other application closed. Which one it was is read from the rejection `getUserMedia` threw, not from the Permissions API — Firefox has no `microphone` name and Safari's answer is unreliable, so a gate that waited for that one never appeared outside Chrome. → services/transcription/microphone.ts
Capability: live
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { unavailable: boolean; busy: boolean }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/mic-denied.tsx
Transcript.Devicesinput · <select>
Which microphone to listen to. A `<select>`, which is the one control that is already the right thing on both ends: a menu beside a pointer and the operating system's own wheel or sheet on a phone, with its keyboard, its type-ahead and its screen-reader behaviour already correct. A popover built out of buttons would be a worse version of all of that on the smaller screen. The list is handed over and the options are drawn one level up, because an `<option>` is a host element and this layer renders none. What the labels say is the browser's — they are data rather than words, and are empty until permission has been given once, which is it declining to be a fingerprint rather than a bug. Three things a picker has to be honest about: - Before that first permission it is also the request. There is one device to list and no names for it, so it is shown anyway and opening it is what asks; the names arrive when the browser has said yes. Hiding it until then meant the first press always used the default, whatever somebody had meant to pick. - While listening it shows the microphone that is actually open, which is not always the one that was chosen, and it is disabled: a change then would be a choice that nothing acts on until the next press, and a select that looks live and is not is worse than one that says so. - Otherwise it is rendered only where there is a choice to make: one microphone and a picker beside it is a control that cannot do anything.
Capability: live
Required scope
Mic.Devices must be inside a transcription root.
Props
2 documented props · Native <select> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { devices: MicDevice[]; active: MicDevice | null; listening: boolean }) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/mic-devices.tsx
Transcript.MissingConfigurationgate
The transcription backend answered that its server lacks a key it needs. Pinned to this family's service so a screen does not repeat the scope, and so another service's missing variable is never its notice. Where the instance can do without the key — a microphone that hides itself when no engine is available — nothing is asked of this gate: the server's warning is what says so. → docs/rules/missing-configuration-is-said.md
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { scopes: readonly string[] }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/missing-configuration.tsx
Transcript.Supportsgate
Draws its children only where this instance can do the thing. The gate exists because capability belongs to the configured instance rather than to the provider: the same key with a different model has different answers, and a screen that assumed would show a speaker column full of nothing. → services/transcription/capabilities.ts
Required scope
Transcript.Supports must be inside a transcription root.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
capabilityRequired | "file" /** Text arrives while somebody is still speaking. */ | "live" /** Revisable hypotheses, not just stable segments. */ | "partials" /** Text can be placed in time, which is what makes a transcript clickable. */ | "timestamps" /** Voices can be told apart. */ | "diarization" /** The language need not be stated in advance. */ | "language-detection" /** * Somebody may change language mid-sentence and be followed. * * Its own capability and not a corollary of `language-detection`, because most models * that detect do it once for the whole recording: they settle on a language and * transcribe the rest of a bilingual meeting in it, which is a worse answer than saying * they cannot. → docs/rules/capability-honesty.md */ | "code-switching" /** Business words and proper nouns can be given in advance. */ | "keywords" /** The transcript can come back in another language. */ | "translation" | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/supports.tsx
Transcript.Lacksgate
Draws its children only where this instance *cannot* do the thing. The other half of `Supports`, and it is not the same screen with the condition flipped: a capability that is absent usually removes an affordance silently, and the cases worth a sentence are the ones where somebody would otherwise be surprised later — a model that hears one language at a time, given a list of three. Saying it is the alternative to pretending. → docs/rules/capability-honesty.md
Required scope
Transcript.Lacks must be inside a transcription root.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
capabilityRequired | "file" /** Text arrives while somebody is still speaking. */ | "live" /** Revisable hypotheses, not just stable segments. */ | "partials" /** Text can be placed in time, which is what makes a transcript clickable. */ | "timestamps" /** Voices can be told apart. */ | "diarization" /** The language need not be stated in advance. */ | "language-detection" /** * Somebody may change language mid-sentence and be followed. * * Its own capability and not a corollary of `language-detection`, because most models * that detect do it once for the whole recording: they settle on a language and * transcribe the rest of a bilingual meeting in it, which is a worse answer than saying * they cannot. → docs/rules/capability-honesty.md */ | "code-switching" /** Business words and proper nouns can be given in advance. */ | "keywords" /** The transcript can come back in another language. */ | "translation" | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/lacks.tsx
Transcript.Configuredgate
Draws its children unless the server has said it cannot transcribe. For what only makes sense with a transcriber behind it — a Captions button, a transcript panel — so a room whose server has no provider key shows neither, rather than a button that turns on captions nobody's words will ever reach and a note ten seconds later saying so. No root needed: a call's controls sit nowhere near its transcriber. Kept while the answer is unknown, and where no instance is installed it hides nothing, because neither is the server saying no. → docs/rules/capability-honesty.md
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/configured.tsx
Transcript.Segmentsscope
Every settled segment, each in its own scope. One part for a live transcript and for a finished one, because a screen that drew them separately would be two transcripts whose rows, timestamps and speaker columns drifted apart — and the second one is usually the first one, revised. A finished transcript comes through the corrections rather than straight from the job, so a line somebody fixed stays fixed — in the list, under the playhead and in the export. → hooks/transcription/use-transcript-edits.ts
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/transcript-segments.tsx
Transcript.Segmentcontent · <li>
One segment's row. `data-state` is how settled it is, which is what a skin fades by: a hypothesis that looked as solid as a final would be a transcript that seems to change its mind.
Required scope
Transcript.Segment must be inside <Transcript.Segments>.
Props
2 documented props · Native <li> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: SegmentState) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/segment.tsx
Transcript.Timecontent · <time>
When a segment starts. Renders nothing where the instance cannot place text in time — a column of zeros would be worse than no column. → docs/rules/capability-honesty.md
Required scope
Transcript.Time must be inside <Transcript.Segments>.
Props
2 documented props · Native <time> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/segment-time.tsx
Transcript.Speakercontent · <span>
Who said it. Nothing where the instance cannot tell voices apart.
Required scope
Transcript.Speaker must be inside <Transcript.Segments>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/segment-speaker.tsx
Transcript.Textcontent · <span>
A segment's words. Text, and not a control. Every line used to be a `<button>` that jumped the audio, and disabled itself where there was nothing to jump to — which is most of the time, because a live transcript has no recording behind it and a submitted job has no player. Two things follow from a button here and both cost more than the click bought: a screen reader reads forty lines as forty dimmed buttons, and a press inside a button activates it rather than starting a selection — so in Chrome and Safari the words cannot be dragged over at all, and the one way to keep a transcript that is kept nowhere was gone. Jumping to a moment is `Transcript.Seek`, on the timestamp, and only where there is a recording to jump in. → components/headless/transcription/segment-seek.tsx
Required scope
Transcript.Text must be inside <Transcript.Segments>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/segment-text.tsx
When something is missing
- No service configuration
- Install the transcription setup below and import "@/services". Required service roots report missing setup; gates do not replace that configuration.
- Missing capability or parent scope
- Follow each part’s context requirements and the example composition. Capability-aware controls hide when the provider cannot perform their action.