Headless / Realtime
Captions and transcript
Build captions & transcript with the room's shared state and capability-aware controls.
10 public parts · 1examples
@slices.sh/realtime/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.
Room.Root · 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/realtime@^0.1.6Import from @slices.sh/realtime/headless.
Install the service
Connect the parts to a provider. Skip this step if this service is already configured in your app.
npx add realtime.p2p-meshAdds 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
Start small
Parts bring the behavior. Use their default elements, or supply your own with render.
"use client"import "@/services"import { Room } from "@slices.sh/realtime/headless"export function Example() { return ( <Room.Root room="studio"> <Room.CaptionsToggle>Toggle captions</Room.CaptionsToggle> <Room.Captions> <Room.CaptionText /> </Room.Captions> <Room.TranscriptLog> <Room.TranscriptLines> <Room.TranscriptLine /> </Room.TranscriptLines> </Room.TranscriptLog> </Room.Root> )}Configure transcription as well when publishing your own captions. The room shares captions; it does not manufacture text when no transcriber is configured.
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.
Room.CaptionsToggleaction · <button>
Read captions, or stop. Yours only; nobody else's screen changes — though while anybody reads them, everybody who talks is transcribed, which is what makes them worth reading.
Capability: transcription: live
Required scope
Room.CaptionsToggle must be inside <Room.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { on: 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/realtime/src/headless/captions-toggle.tsx
Room.Captionsscope
What people are saying, as their own transcribers heard it — each line in its speaker's scope, so `Room.PeerName` and `Room.PeerAvatar` say who. Nothing while captions are off.
Capability: transcription: live
Required scope
Room.Captions must be inside <Room.Root>.
Props
4 documented props
| Prop | Type | Default |
|---|---|---|
limitThe most recent lines to show, by who spoke last. One is a caption bar; more is a log. | number | 1 |
groupWhat the lines are grouped in: a Motion presence group lets a line leave rather than vanish. | GateGroupComponent | — |
selfWhether your own words are among the lines. Off hides them, and the limit counts what is left: a screen that hid yours after choosing the latest line would show nothing at all while you spoke over somebody. | boolean | true |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/captions.tsx
Room.CaptionTextcontent · <span>
The words of one caption line. `data-pending` while the speaker's transcriber may still change them.
Capability: transcription: live
Required scope
Room.CaptionText must be inside <Room.Captions>.
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/realtime/src/headless/caption-text.tsx
Room.TranscriptLogcontent · <div>
The scrolling region the room's spoken words collect in: a call's transcript, rather than the caption line of the moment. A log, for the reasons `Room.Transcript` is one: a screen reader hears a sentence as it settles, and a region that scrolls has to be a tab stop or a keyboard cannot reach what is above the fold. It follows the newest line while it is at the bottom and lets go the moment somebody scrolls up; `Room.NewMessages` and `Room.ScrollToLatest` inside it say what arrived meanwhile. `data-state` is `pinned` or `scrolled`.
Capability: transcription: live
Required scope
Room.TranscriptLog must be inside <Room.Root>.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { pinned: boolean; unseen: 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/realtime/src/headless/transcript-log.tsx
Room.TranscriptLinesscope
Everything said since you joined, oldest first, each line in its speaker's scope so `Room.PeerName` and `Room.PeerAvatar` say who and `Room.CaptionText` says what. What is being said right now follows the settled lines, one per speaker, until its transcriber settles it. A speaker who has left keeps their lines, under the name they were heard with: a transcript that lost a line when its speaker hung up would be a transcript with holes in it.
Capability: transcription: live
Required scope
Room.TranscriptLines must be inside <Room.Root>.
Props
3 documented props
| Prop | Type | Default |
|---|---|---|
groupWhat the lines are grouped in: a Motion presence group lets a new line arrive rather than appear. | GateGroupComponent | — |
selfWhether your own words are among the lines. Off hides them, as it does for | boolean | true |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/transcript-lines.tsx
Room.TranscriptLinecontent · <li>
One line of the transcript. `data-state` is `partial` while its speaker's transcriber may still change the words and `final` once it will not, which is what a skin fades by. A partial line is hidden from a screen reader: the log announces what is added, and announcing a sentence word by word and then again whole is a transcript read twice.
Capability: transcription: live
Required scope
Room.TranscriptLine must be inside <Room.TranscriptLines>.
Props
2 documented props · Native <li> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { final: 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/realtime/src/headless/transcript-line.tsx
Room.NoTranscriptgate
Nothing has been said since you joined. `shown` is whether you read captions: a transcript that is empty because captions are off wants a different sentence from one nobody has spoken into yet, since only the second will fill by itself.
Capability: transcription: live
Required scope
Room.NoTranscript must be inside <Room.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { shown: boolean }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/no-transcript.tsx
Room.Transcribergate
Whether this browser should be transcribing its own voice right now, for a transcriber to follow — `Transcript.Listen`, say — without the realtime family reaching into that one. Only while somebody in the room reads captions, and only while the microphone is live: what somebody says on mute was never said to the room, and sending it to a transcription provider anyway would be a leak with a caption on it. And from the microphone the room hears, which a device picker may have changed.
Capability: transcription: live
Required scope
Room.Transcriber must be inside <Room.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { needed: boolean; device: string | null }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/transcriber.tsx
Room.PublishCaptionscope
Hands what this browser's own transcriber heard to everybody in the room, as captions. Renders nothing. Fed from a transcript's parts — `Transcript.Heard` — so the realtime family never reaches into the transcription family, and a room without a transcriber simply sends no captions. Only a change is sent: a hypothesis as it grows, and the settled line once.
Capability: transcription: live
Required scope
Room.PublishCaption must be inside <Room.Root>.
Props
3 documented props
| Prop | Type | Default |
|---|---|---|
textRequired | string | — |
finalRequired | boolean | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/publish-caption.tsx
Room.CaptionsNeededgate
Somebody is reading captions and this microphone is live: this browser's words are wanted. Where a screen says so when nothing can transcribe them, rather than leave the readers wondering why one voice never appears.
Capability: transcription: live
Required scope
Room.CaptionsNeeded must be inside <Room.Root>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
graceHow long the need has to stand before it shows, in milliseconds. A transcriber that is still arriving, a chunk loading or a token being fetched, looks to the room exactly like one that never will, and a sentence about a failure that has not happened is worse than a late one. | number | 10_000 |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/captions-needed.tsx
Room.Rootscope
The room everything under here is in. Joining happens in the provider's effect rather than here, so a component that renders twice before it commits does not put two peers in the roster for one tab. A root inside a root for the same room shares its connection, and its follow too: a presence list placed on a canvas has to move the canvas when somebody on it is picked, and two follows would be two lists of whom somebody is following. A root inside a root for the same room needs no service of its own, and says nothing when there is none. Optional with no service, or off, there is no room: its status is "off", so every gate stays shut and `Room.Present` hides what the room is drawn inside, and its sink is switched off, so a Composer inside renders nothing. → ../hooks/use-realtime-scope.ts
Props
8 documented props
| Prop | Type | Default |
|---|---|---|
configThe realtime config, a slug from the registry ( | string | — |
enabledLeft out or | Enabled | — |
roomRequired | string | — |
profileWho this browser shows itself as. Without one, a visitor gets an animal of their own; and somebody signed in is their session's name and picture whatever this says. | PeerProfile | — |
perPersonOne person across this browser's tabs: every tab takes the same name, the one a screen that counts people shows once. Off, each tab is somebody of its own. | boolean | — |
mediaOff, the transport never asks for a camera or a microphone. | boolean | — |
captionsCaptions start shown; each person can still hide them for themselves. | boolean | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/room-root.tsx
Room.PeerAvatarcontent · <span>
A peer's face: their picture once it has loaded, their initials until then and for good if it never does, with their colour as `--peer` and the ink that reads on it as `--peer-ink`, for the skin to paint with. → ./peer-look.ts The picture is painted as the face's background and the initials are dropped when it arrives, so a skin's size, shape and colour work unchanged and a slow or dead address costs nothing but the picture. → ./use-peer-picture.ts Named after the person rather than after the two letters standing in for them: a pile of these is the whole of a presence list, and without a name it reads out as "AB CD EF". The name is the peer's own and is never a word a skin or a copy chooses, so it is locked here rather than asked for. (WCAG 1.1.1)
Required scope
Room.PeerAvatar must be inside <Room.Peers>.
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/realtime/src/headless/peer-avatar.tsx
Room.PeerNamecontent · <span>
Peers repeats the people here, up to max, and Peer is one scope. PeerName, PeerAvatar (with --peer colour), PeerStatus (the data path, in words), PeerActivity (here, idle, away) describe the peer in scope. PeerCount counts them. IsMe, NotMe and Me pick out you. MorePeers gives { hidden, names } for the ones a pile leaves out. Unreachable renders for a peer this browser has no route to, which needs a relay. Roster, RosterSearch and NoMatches make a searchable list for a big room.
Required scope
Room.PeerName must be inside <Room.Peers>.
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/realtime/src/headless/peer-name.tsx
When something is missing
- No service configuration
- Install the realtime 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.