Headless / Realtime
Team conversation
Build room chat with the room's shared state and capability-aware controls.
33 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.6 @slices.sh/composer@^0.1.4Import 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"import { Composer } from "@slices.sh/composer"export function Example() { return ( <Room.Root room="studio"> <Room.Messages> <Room.Message> <Room.MessageAuthor /> <Room.MessageBody /> </Room.Message> </Room.Messages> <Composer.Root> <Composer.Input aria-label="Message" /> <Composer.Submit>Send</Composer.Submit> </Composer.Root> </Room.Root> )}Room.Root receives the Composer message and publishes it to the room.
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.Transcriptcontent · <div>
The scrolling region a conversation arrives in. A log, so a screen reader reads a message as it lands rather than never: `role="log"` is the one live region defined for exactly this, and it announces additions instead of re-reading the transcript. `data-state` is `pinned` or `scrolled`, which is what a skin shows a "jump to latest" over. The region rather than the list, and the messages keep a `<ul>` of their own inside it: a role replaces whatever the element was, so a `<ul role="log">` is a list no longer — the bubbles inside it stop being items and a screen reader reads the conversation as one run of text with no boundaries in it. (WAI-ARIA, "Presentational Children" and role conflict) A tab stop, because a region that scrolls and cannot be focused cannot be scrolled by anybody using a keyboard: the whole conversation above the fold is unreachable, which is WCAG 2.1.1 and the reason a chat log is the APG's own example of this. It needs a name, and the name is a word, so the screen gives it. (WCAG 2.4.3, 4.1.2)
Required scope
Room.Transcript 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.tsx
Room.NoMessagesgate
Nothing has been said since you arrived, which for this service is the usual start.
Required scope
Room.NoMessages must be inside <Room.Root>.
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/realtime/src/headless/no-messages.tsx
Room.Timelinescope
The conversation in order: each turn of one person's messages, and each marker between — somebody arriving, leaving, starting to present. Each item gates itself: `Room.Run` draws a turn and `Room.Marker` a marker, so the list is listed once and branches nowhere.
Required scope
Room.Timeline must be inside <Room.Root>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
groupWhat the items are grouped in: a Motion presence group lets a message arrive, not appear. | ComponentType<{ children?: ReactNode }> | — |
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/timeline.tsx
Room.Markercontent · <li>
Something that happened between messages: somebody arrived, left or started presenting. `data-state` is the event. The words are the pattern's, from the state.
Required scope
Room.Marker must be inside <Room.Timeline>.
Props
2 documented props · Native <li> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: ChatMarker) => 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/marker.tsx
Room.Runcontent · <li>
One person's turn: their consecutive messages within five minutes, under one name. The first message is in scope for the whole turn, so `Room.MessageAuthor`, `Room.MessageAvatar` and `Room.MessageTime` placed on the turn say who and when once. `data-state` is `mine` or `theirs`, which is what a skin sides a turn by.
Required scope
Room.Run must be inside <Room.Timeline>.
Props
2 documented props · Native <li> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { mine: 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/run.tsx
Room.RunMessagesscope
The messages of one turn, each in its own scope with where it is: sending, sent or failed.
Required scope
Room.RunMessages must be inside <Room.Run>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
groupWhat the messages are grouped in: a Motion presence group lets each one arrive. | ComponentType<{ children?: ReactNode }> | — |
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/run-messages.tsx
Room.Messagecontent · <li>
One message. `data-state` is `mine` or `theirs`, which is what a skin sides by; a message of yours still on its way is `data-pending`, and one that never went is `data-invalid`. `data-flash` is set for a moment when a quote of it is followed here.
Required scope
Room.Message must be inside <Room.Messages>.
Props
2 documented props · Native <li> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { mine: 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/message.tsx
Room.MessageAuthorcontent · <span>
Who said it. Looked up in the roster rather than carried on the message, because a name the sender put in the message would be the name they had when they sent it — and a rename mid-conversation would leave half the transcript addressed to somebody who no longer exists.
Required scope
Room.MessageAuthor must be inside <Room.Messages>.
Room.MessageAuthor must be inside <Room.Root>.
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/message-author.tsx
Room.MessageAvatarcontent · <span>
The face of whoever said it, their picture or else their initials, with their colour as `--peer` and its ink as `--peer-ink`. Its own part rather than `Room.PeerAvatar` inside a message, because a message outlives the person: somebody who left is still in the transcript and is in nobody's roster, and a part that needed a peer scope would have to throw for them. Here they keep a colour derived from the id the message carries, so the conversation does not change shape when they close the tab. → ./peer-look.ts
Required scope
Room.MessageAvatar must be inside <Room.Messages>.
Room.MessageAvatar must be inside <Room.Root>.
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/message-avatar.tsx
Room.MessageBodycontent · <span>
Transcript is the chat's scroller, role="log". Timeline lists turns and markers in order; Run is one person's consecutive messages under one name and RunMessages its messages; Marker is an arrival, a departure or a presentation. Messages and Message are the flat alternative. MessageAuthor, MessageAvatar, MessageBody and MessageTime describe the message in scope, and MessageSegments splits its text into SegmentText and SegmentLink. NoMessages, NewMessages and ScrollToLatest handle the empty list and the reader who scrolled up. Nothing is kept: the chat lives as long as the room.
Required scope
Room.MessageBody must be inside <Room.Messages>.
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/message-body.tsx
Room.MessageTimecontent · <time>
When it was said. `RoomMessage.at` has been on the wire since the first version of this service and no screen ever showed it. A `<time>` with the full moment in `dateTime` and the hour and minute as text: the long form is what assistive technology and a copy-paste get, and the short form is what a bubble has room for.
Required scope
Room.MessageTime must be inside <Room.Messages>.
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/realtime/src/headless/message-time.tsx
Room.MessageSegmentsscope
A message's text as words and links, each in its own scope.
Required scope
Room.MessageSegments must be inside <Room.Messages>.
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/realtime/src/headless/message-segments.tsx
Room.SegmentTextcontent
Words, as they were typed. Nothing for a link, which `Room.SegmentLink` draws.
Required scope
Room.SegmentText must be inside <Room.MessageSegments>.
Props
2 documented props
| 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/segment-text.tsx
Room.SegmentLinkcontent · <a>
A link somebody sent, opened somewhere else: a chat that navigated away from itself on a click would be a meeting somebody just left. `rel` keeps the other page from reaching back.
Required scope
Room.SegmentLink must be inside <Room.MessageSegments>.
Props
2 documented props · Native <a> 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/segment-link.tsx
Room.Quoteaction · <button>
What this message answers, as it was when it was answered: a button that scrolls to the original while the list still holds it. Nothing on a message that answers nobody. The name is looked up in the roster, like an author's, and the one the quote carried is for somebody who has since left. → ./message-author.tsx
Required scope
Room.Quote must be inside <Room.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: QuoteState) => 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/quote.tsx
Room.Replyaction · <button>
Answer this message: the composer quotes it, and the field takes the cursor.
Required scope
Room.Reply must be inside <Room.RunMessages>.
Room.Reply must be inside <Room.Root>.
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/realtime/src/headless/reply.tsx
Room.Sendinggate
This message of yours is waiting for the room: reconnecting, or not joined yet.
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/realtime/src/headless/sending.tsx
Room.NotSentgate
This message of yours never found its way to the room, and is still here to try again.
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/realtime/src/headless/not-sent.tsx
Room.Retryaction · <button>
Send a failed message again, under the id it already has, so it cannot arrive twice.
Required scope
Room.Retry must be inside <Room.RunMessages>.
Room.Retry must be inside <Room.Root>.
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/realtime/src/headless/retry.tsx
Room.MessageReactionsscope
The emoji left on this message, each once with everybody who left it.
Required scope
Room.MessageReactions must be inside <Room.Messages>.
Room.MessageReactions must be inside <Room.Root>.
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/realtime/src/headless/message-reactions.tsx
Room.ReactionToggleaction · <button>
One emoji on a message and how many left it: pressing it adds yours or takes it back.
Required scope
Room.ReactionToggle must be inside <Room.MessageReactions>.
Room.ReactionToggle must be inside <Room.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: ReactionState) => 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/reaction-toggle.tsx
Room.ReactToaction · <button>
Leave this emoji on the message in scope, or take it back.
Required scope
Room.ReactTo must be inside <Room.Messages>.
Room.ReactTo must be inside <Room.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
emojiRequired | string | — |
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/react-to.tsx
Room.Messagesscope
What has arrived while this browser has been in the room, and nothing else. A late joiner sees an empty list, which is what the service promises rather than a bug. → services/realtime/ports.ts
Required scope
Room.Messages must be inside <Room.Root>.
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/realtime/src/headless/messages.tsx
Room.NewMessagesgate
Something arrived while somebody was reading further up. Only then: a button offering to jump to the bottom of a list that is already at the bottom is a button that does nothing, and one offering "new messages" when none have arrived is a lie. → docs/rules/capability-honesty.md
Required scope
Room.NewMessages must be inside <Room.Transcript>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { unseen: number }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/new-messages.tsx
Room.ScrollToLatestaction · <button>
Back to the newest message, and following again from there.
Required scope
Room.ScrollToLatest must be inside <Room.Transcript>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { 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/scroll-to-latest.tsx
Room.Composeraction · <form>
The one form a message is sent from. It reads its input by name on submit rather than tracking every keystroke, so the input stays uncontrolled and a skin may put anything around it.
Required scope
Room.Composer must be inside <Room.Root>.
Props
2 documented props · Native <form> 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/composer.tsx
Room.ReplyDraftcontent · <div>
Where a composer says which message it is answering, for as long as it is: nothing while it writes to everybody. `Room.ReplyingTo` names the message and `Room.CancelReply` lets it go. Answering another message is a new draft, so it arrives as one.
Required scope
Room.ReplyDraft must be inside <Room.Root>.
Props
3 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
group | GateGroupComponent | — |
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/reply-draft.tsx
Room.ReplyingTocontent · <span>
The message being answered: who said it and a line of what they said, for its children.
Required scope
Room.ReplyingTo must be inside <Room.Root>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: ReplyingState) => 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/replying-to.tsx
Room.CancelReplyaction · <button>
Write to everybody again rather than answer one message. Escape in the field does the same.
Required scope
Room.CancelReply must be inside <Room.Root>.
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/realtime/src/headless/cancel-reply.tsx
Room.MessageInputinput · <input>
What is being typed. `name="message"` is what the composer reads.
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/realtime/src/headless/message-input.tsx
Room.MessageFieldinput · <textarea>
What is being typed, over as many lines as it takes: Enter sends, Shift+Enter breaks the line, and nothing is sent in the middle of composing a character with an input method. `name="message"` is what the composer reads. Typing here tells the room you are. Choosing a message to answer brings the cursor here, and Escape stops answering it.
Props
2 documented props · Native <textarea> 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/message-field.tsx
Room.Sendaction · <button>
The one submit in the composer: everything else beside it is `type="button"`. → docs/rules/one-submit-per-form.md Enabled while the room is joining or reconnecting as well as connected, because a message typed then is kept and sent the moment the room is back, drawn as sending until it goes. Disabled only where it could never go: a room that refused, failed or was left. → docs/rules/capability-honesty.md
Required scope
Room.Send must be inside <Room.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { connected: 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/send.tsx
Room.Typinggate
Somebody else is typing, and who. Not announced: a live region saying it per keystroke is noise, and the message itself is what gets said when it lands. Your other tab is you, whether or not the screen counts people: a line saying you are typing tells you nothing, and under your own name it reads "You is typing".
Required scope
Room.Typing must be inside <Room.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { names: string[] }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/typing.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
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.