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

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.

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

Room.Root · Scope

Room.RootThe room everything under here is in.

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/realtime@^0.1.6

Import from @slices.sh/realtime/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 realtime.p2p-mesh

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

    Start small

    Parts bring the behavior. Use their default elements, or supply your own with render.

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

    Room.CaptionsToggle props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { on: 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/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

    Room.Captions props
    PropTypeDefault
    limit

    The most recent lines to show, by who spoke last. One is a caption bar; more is a log.

    number1
    group

    What the lines are grouped in: a Motion presence group lets a line leave rather than vanish.

    GateGroupComponent—
    self

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

    booleantrue
    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/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

    Room.CaptionText 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/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

    Room.TranscriptLog props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { pinned: boolean; unseen: number }) => 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/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

    Room.TranscriptLines props
    PropTypeDefault
    group

    What the lines are grouped in: a Motion presence group lets a new line arrive rather than appear.

    GateGroupComponent—
    self

    Whether your own words are among the lines. Off hides them, as it does for Room.Captions.

    booleantrue
    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/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

    Room.TranscriptLine props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { final: 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/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

    Room.NoTranscript props
    PropTypeDefault
    children

    Content 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

    Room.Transcriber props
    PropTypeDefault
    children

    Content 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

    Room.PublishCaption props
    PropTypeDefault
    textRequired
    string—
    finalRequired
    boolean—
    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/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

    Room.CaptionsNeeded props
    PropTypeDefault
    grace

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

    number10_000
    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/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

    Room.Root props
    PropTypeDefault
    config

    The realtime config, a slug from the registry (services/realtime/<config>.config.ts). The nearest root's, else "default", when left out.

    string—
    enabled

    Left out or true: required, and a missing service throws. "optional": renders its children, its parts render nothing, and its sink is switched off. false: off. A capability name ("relay") requires the service and that capability.

    Enabled—
    roomRequired
    string—
    profile

    Who 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—
    perPerson

    One 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—
    media

    Off, the transport never asks for a camera or a microphone.

    boolean—
    captions

    Captions start shown; each person can still hide them for themselves.

    boolean—
    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/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

    Room.PeerAvatar 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/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

    Room.PeerName 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/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.
    Read the setup guide →