Skip to content
SLICES / TECHNICAL FOLIO Nº 001FULL-STACK ASSEMBLIES — OPEN SOURCE

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.

Behavior / inputGateGenericRenderer · not headlessScope
COMPONENT ANATOMYHover · focus · tap
ONE POSSIBLE DESIGNStatic preview

Transcript.Live · Scope

Transcript.LiveLive 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.

Installation

Choose what your component needs. Run these commands in an initialized Next.js app.

0

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 nova

Or start fresh: the slices CLI creates the app and sets it up.

npx slices.sh install
1

Install 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 services
2

Install the headless library

npm

Add the React parts to your app, then compose them with your own elements.

npm install @slices.sh/transcription@^0.1.11

Import from @slices.sh/transcription/headless.

3

Install the service

Connect the parts to a provider. Skip this step if this service is already configured in your app.

Provider
npx add transcription.browser

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

example.tsx
"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

Transcript.Live props
PropTypeDefault
config
string—
enabled
Enabled—
languages
string[]—
keywords
string[]—
maxDurationMs
number—
children

Content 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

Transcript.LiveToggle props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { listening: boolean }) => ReactNode)—
render

Replace 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

Transcript.Listen props
PropTypeDefault
whenRequired
boolean—
device

The microphone to hear, when something else already chose one; null is the browser's.

string | null—
children

Content 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

Transcript.LiveStatus props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: Announced) => ReactNode)—
render

Replace 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

Transcript.LiveIdle props
PropTypeDefault
children

Content 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

Transcript.LiveFailed props
PropTypeDefault
children

Content 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

Transcript.Partial props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace 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

Transcript.Heard props
PropTypeDefault
children

Content 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

Transcript.Log props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { following: boolean }) => ReactNode)—
render

Replace 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

Transcript.Latest props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace 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

Transcript.Reconnecting props
PropTypeDefault
children

Content 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

Transcript.MicDenied props
PropTypeDefault
children

Content 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

Transcript.Devices props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { devices: MicDevice[]; active: MicDevice | null; listening: boolean }) => ReactNode)—
render

Replace 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

Transcript.MissingConfiguration props
PropTypeDefault
children

Content 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

Transcript.Supports props
PropTypeDefault
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"—
children

Content 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

Transcript.Lacks props
PropTypeDefault
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"—
children

Content 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

Transcript.Configured props
PropTypeDefault
children

Content 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

Transcript.Segments props
PropTypeDefault
children

Content 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

Transcript.Segment props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: SegmentState) => ReactNode)—
render

Replace 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

Transcript.Time props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace 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

Transcript.Speaker props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace 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

Transcript.Text props
PropTypeDefault
children

Content rendered inside this part.

ReactNode—
render

Replace 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.
Read the setup guide →