Headless / Transcription
Recording transcript
Upload a recording, follow transcription progress and inspect the completed result.
29 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.Job · 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.deepgramAdds 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
- Run: npx extraorbital provision deepgram, which sets DEEPGRAM_REFRESH_TOKEN, DEEPGRAM_TOKEN_URL and DEEPGRAM_BASE_URL
- jobs "redis" keeps job records in a table every instance can see: set REDIS_URL (npx extraorbital provision redis). Without it the records stay in this process, and a restart loses them.
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.Job> <Transcript.JobIdle> <Transcript.JobDrop> Choose or drop a recording <Transcript.JobPicker aria-label="Recording" /> </Transcript.JobDrop> </Transcript.JobIdle> <Transcript.JobRunning> <Transcript.JobProgress /> <Transcript.CancelJob>Cancel</Transcript.CancelJob> </Transcript.JobRunning> <Transcript.JobDone> <Transcript.Segments> <Transcript.Segment> <Transcript.Text /> </Transcript.Segment> </Transcript.Segments> </Transcript.JobDone> <Transcript.JobFailed> <p>Transcription failed.</p> <Transcript.Retry>Try again</Transcript.Retry> </Transcript.JobFailed> </Transcript.Job> )}Use a provider with file transcription (for example OpenAI, Deepgram or Gladia). The browser recognizer only supports live speech.
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.Jobscope
A recording submitted and waited for.
Capability: file
Props
4 documented props
| Prop | Type | Default |
|---|---|---|
config | string | — |
enabled | Enabled | — |
timestampsHow finely the transcript should be placed in time.
| "none" | "segment" | "word" | "segment" |
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/job-root.tsx
Transcript.JobStatuscontent · <p>
What a submitted recording tells a screen reader. Always mounted, for the reason the other two announcers are: a live region that appears with its content already in it announces nothing. The card that said "Transcribing…" carried `role="status"` and appeared exactly that way, so the upload said nothing, and the transcript arriving — which is the entire point of the screen, after a wait long enough to have gone and done something else — said nothing either. → components/headless/transcription/dictation-status.tsx
Capability: file
Required scope
Transcript.JobStatus must be inside <Transcript.Job>.
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/job-status.tsx
Transcript.JobDropcontent · <label>
A dashed box that actually takes a dropped recording. The dashed border is the universal promise of drag-and-drop, and both job screens drew one around a file input that ignored a drop entirely — so the one gesture the shape invites did nothing at all. It is a `<label>` around that input rather than a div with handlers, so the keyboard and the screen reader still get a real control: dropping is the shortcut, and choosing is the way. Only the first file, and whatever it is. A second one would need a queue, and a queue that silently transcribed the wrong recording first is worse than a screen that takes one at a time. Sorting audio from everything else is the job's, because a dropzone that quietly kept nothing is indistinguishable from one that is broken — and because a recording whose type the browser cannot name is most `.m4a` files rather than a mistake. → hooks/transcription/use-transcription-job.ts
Capability: file
Required scope
Transcript.JobDrop must be inside <Transcript.Job>.
Props
2 documented props · Native <label> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { over: 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/job-drop.tsx
Transcript.JobPickerinput · <input>
The audio file to transcribe.
Capability: file
Required scope
Transcript.JobPicker must be inside <Transcript.Job>.
Props
2 documented props · Native <input> 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/job-picker.tsx
Transcript.JobProgresscontent · <progress>
How far along a job is. Indeterminate when the provider does not say — most do not — because a bar sitting at zero for four minutes looks exactly like one that is stuck. → docs/rules/capability-honesty.md
Capability: file
Required scope
Transcript.JobProgress must be inside <Transcript.Job>.
Props
2 documented props · Native <progress> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { progress: number | null }) => 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/job-progress.tsx
Transcript.JobLimitcontent · <span>
The largest recording one request can carry. Rendered only where the instance published a number. A ceiling is worth saying in advance — it is the difference between choosing a shorter file and uploading an hour of audio to be told it was always too big — and a box that announced "up to 0 MB" because nobody knew would be worse than one that says nothing. → docs/rules/capability-honesty.md
Capability: file
Required scope
Transcript.JobLimit must be inside <Transcript.Job>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { megabytes: number }) => 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/job-limit.tsx
Transcript.JobIdlegate
Nothing submitted yet: where the invitation to choose a file goes.
Capability: file
Required scope
Transcript.JobIdle must be inside <Transcript.Job>.
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/job-idle.tsx
Transcript.JobRunninggate
Uploading, or waiting for the provider. `uploading` is told apart because the two wait for different things: one is this browser's link, the other is somebody else's queue, and only the first has a size.
Capability: file
Required scope
Transcript.JobRunning must be inside <Transcript.Job>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { uploading: boolean }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/transcription/src/headless/job-running.tsx
Transcript.JobDonegate
There is a transcript.
Capability: file
Required scope
Transcript.JobDone must be inside <Transcript.Job>.
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/job-done.tsx
Transcript.JobEmptygate
It came back, and there was nothing in it. A recording of a quiet room transcribes to no segments at all, and the screen drew a card with an empty list in it — which reads as a transcript that failed to load rather than as a recording with nothing said in it. The provider did its job and the answer is the answer. → docs/rules/states-gate-themselves.md
Capability: file
Required scope
Transcript.JobEmpty must be inside <Transcript.Job>.
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/job-empty.tsx
Transcript.JobStoppedgate
Somebody stopped waiting. Its own gate, and not the failure one, because they are different news: the provider did nothing wrong, the audio it already has is still billed, and a red banner reading "the service did not answer" blames it for a decision it knew nothing about.
Capability: file
Required scope
Transcript.JobStopped must be inside <Transcript.Job>.
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/job-stopped.tsx
Transcript.JobFailedgate
It was refused, with the reason. `message` is the service's own words for it, `null` where it had none.
Capability: file
Required scope
Transcript.JobFailed must be inside <Transcript.Job>.
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/job-failed.tsx
Transcript.CancelJobaction · <button>
Stops waiting. Not "stops transcribing": the request is already at the provider and will be billed. The copy beside this should say so rather than promising a refund nobody arranged.
Capability: file
Required scope
Transcript.CancelJob must be inside <Transcript.Job>.
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/cancel-job.tsx
Transcript.Retryaction · <button>
Back to the invitation. Not "try again with the same file": a stopped job has already been billed and a refused one was usually refused for something about the recording, so the useful next step is choosing. Rendered from every state a job is finished in — including the one where it worked, because a transcript on screen is exactly when somebody has the next recording ready and the dropzone is gone.
Capability: file
Required scope
Transcript.Retry must be inside <Transcript.Job>.
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/job-retry.tsx
Transcript.Playbackscope
The transcript following the audio, and the audio following a click on the transcript. The recording is the one the job was given: it never left the browser on its way here, so playing it back costs nothing and needs no URL from anybody. Revoked on unmount, because an object URL that outlives its page holds the whole file in memory.
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/playback-root.tsx
Transcript.Audiocontent · <audio>
The audio element itself. Rendered only once there is something to play: an `<audio>` with no source shows a disabled player, which reads as broken rather than as empty.
Required scope
Transcript.Audio must be inside <Transcript.Playback>.
Props
2 documented props · Native <audio> 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/playback-audio.tsx
Transcript.PlayToggleaction · <button>
Plays or pauses. `data-active` while it is playing.
Required scope
Transcript.PlayToggle must be inside <Transcript.Playback>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { playing: 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/play-toggle.tsx
Transcript.Rateinput · <select>
How fast the recording plays. A `<select>`, which on a phone is the operating system's own wheel and beside a pointer is a menu — both already keyboard-operable and both already announced properly. Checking a transcript against an hour of audio at one times real time is the reason nobody does it. The rates are handed over and the options are drawn one level up: an `<option>` is a host element, and how a multiplier is written — "1.5×", "1.5x", "150%" — is words. → docs/rules/words-in-copy-looks-in-skin.md
Required scope
Transcript.Rate must be inside <Transcript.Playback>.
Props
2 documented props · Native <select> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { rates: readonly number[]; rate: number }) => 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/playback-rate.tsx
Transcript.Exportaction · <button>
Saves the transcript as a file. A live transcript as well as a finished one, and for the live one it is the only way out there is: the session is held in this page and in nothing else, which the screen says out loud — so a screen that said it and then offered no way to keep the words was telling somebody their meeting was about to be lost and handing them nothing. `srt` and `vtt` are timed formats, so the button is not rendered at all where the transcript has no times — a subtitle file whose every cue starts at zero is worse than no subtitle file. → docs/rules/capability-honesty.md
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
formatRequired | "txt" | "srt" | "vtt" | — |
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/export-transcript.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.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
Transcript.Editinput · <input>
A line somebody can fix. A model mishears a name, a number or a term of art two or three times an hour, and a transcript nobody can correct is a transcript that stays wrong — including in the subtitle file it exports. So this is a real text input over the segment, and the export reads the corrected text rather than what came back. Nothing is sent anywhere: no instance implements `update`, so the fix lives as long as the page and a button offering to "save" it would be lying. → docs/plans/next-families/transcription.md
Required scope
Transcript.Edit must be inside <Transcript.Segments>.
Transcript.Edit must be inside <Transcript.Job>.
Props
2 documented props · Native <input> 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-edit.tsx
Transcript.Seekaction · <button>
Move the audio to where this line starts. Its own control because the line's text stops being one as soon as the line is editable: a click inside an input has to put the caret there, so the way to that moment in the recording moves onto the timestamp beside it. Rendered only where there is a time to jump to, like everything else here: an instance without timestamps produces a transcript that is simply not clickable rather than one that is and takes you to the wrong place. → docs/rules/capability-honesty.md
Required scope
Transcript.Seek must be inside <Transcript.Segments>.
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/segment-seek.tsx
Transcript.Wordsscope
One scope per timed word, for a transcript that lights up as it plays. `Word { text, startMs, endMs }` has been in the contract from the beginning and one backend has populated it for `timestamps: "word"` all along with nothing anywhere reading it — so a transcript highlighted whole lines while the times to do better sat in the data. → services/transcription/transcript.ts An instance that did not time words gets one scope for the whole line instead, carrying the line's own times. The same markup then draws a transcript that highlights by line, which is what it did before, rather than an empty row — and nothing is ever lit from a time nobody measured. → docs/rules/capability-honesty.md
Required scope
Transcript.Words must be inside <Transcript.Segments>.
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/segment-words.tsx
Transcript.Wordcontent · <span>
One word, marked while the playhead is inside it. `data-active` and nothing else: what a highlight looks like is the skin's, and a headless part that chose a colour would be making a decision one level too low. → docs/rules/words-in-copy-looks-in-skin.md
Required scope
Transcript.Word must be inside <Transcript.Words>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { current: 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/segment-word.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.SpeakerNameinput · <input>
A speaker's name, renamed everywhere at once. Diarization hands back "Speaker 1" and "Speaker 2", and the only thing anybody wants to do with those is put the real names in. One at a time is forty edits and one line still reading "Speaker 2", so this renames every segment that speaker said. → hooks/transcription/use-transcript-edits.ts
Required scope
Transcript.SpeakerName must be inside <Transcript.Segments>.
Transcript.SpeakerName must be inside <Transcript.Job>.
Props
2 documented props · Native <input> 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/speaker-name.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.