Headless / Realtime
Follow a teammate
Build cursors & following with the room's shared state and capability-aware controls.
14 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.CursorArea> <Room.CursorSurface /> <Room.Cursors> <Room.Cursor> <Room.CursorLabel /> </Room.Cursor> </Room.Cursors> <p>Move your pointer here.</p> </Room.CursorArea> <Room.StopFollowing>Stop following</Room.StopFollowing> </Room.Root> )}Configure realtime. Media, captions and moderation controls hide when the provider does not grant their capability.
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.CursorAreascope
A cursor surface and what is drawn around it — the chips for pointers scrolled out of view — which need the scrolling element itself.
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/cursor-area.tsx
Room.CursorSurfacecontent · <div>
The area pointers are shared over, and the thing that scrolls under them. Positions travel as fractions of this element's content, so two people with different window sizes see the pointer in the same place, and scrolling never moves a pointer off what it points at. It has to be positioned for the cursors inside it to sit anywhere, which is the skin's business and the one thing a skin here must not forget. Following somebody drifts it to keep their pointer in the middle; anything you do yourself — a wheel, a press, Escape — gives the view back. `/` opens a bubble at your pointer, where the pattern offers cursor chat.
Required scope
Room.CursorSurface must be inside <Room.Root>.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { following: 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/cursor-surface.tsx
Room.Cursorsscope
Everyone else's pointer, each in its own scope. Yours is never in here.
Required scope
Room.Cursors must be inside <Room.Root>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
perPersonOne pointer per person — whichever of their tabs moved last — and none from your own other tabs, which are you. Off, every tab is a pointer of its own. | boolean | false |
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/cursors.tsx
Room.Cursorcontent · <div>
One pointer, gliding between the samples the network gives it on a spring of the page's one frame loop. → ./use-pointer-spring.ts Just after somebody is picked to follow it says `data-state="pinged"` for a moment, which a skin rings so the person can find the arrow on a busy page. At rest it says so on the element: `data-idle="label"` after three seconds, which a skin folds the name away on, and `"dim"` after ten. Positions are fractions of the surface's content, so a pointer on a paragraph stays on that paragraph when you scroll.
Required scope
Room.Cursor must be inside <Room.Cursors>.
Room.Cursor must be inside <Room.Root>.
Props
2 documented props · Native <div> 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/cursor.tsx
Room.CursorLabelcontent · <span>
Whose pointer it is. Their colour is `--peer` and the ink that reads on it `--peer-ink`: white on a dark colour and near-black on a light one, never white on yellow.
Required scope
Room.CursorLabel must be inside <Room.Cursors>.
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/cursor-label.tsx
Room.CursorMessagecontent · <span>
What somebody typed at their pointer, for the few seconds it is said; `--peer` is their colour.
Required scope
Room.CursorMessage must be inside <Room.Cursors>.
Room.CursorMessage must be inside <Room.Root>.
Props
2 documented props · Native <span> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { text: string }) => 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/cursor-message.tsx
Room.CursorChatcontent · <div>
Your own bubble at your pointer: open while you type, and holding what you said for a few seconds after. It follows the pointer while it is a draft and stays put once sent, where the words were said.
Required scope
Room.CursorChat must be inside <Room.Root>.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { sent: 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/cursor-chat.tsx
Room.CursorChatInputinput · <input>
What you are about to say at your pointer. Enter says it; Escape takes it back.
Required scope
Room.CursorChatInput must be inside <Room.Root>.
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/cursor-chat-input.tsx
Room.CursorChatSentgate
What you said at your pointer, for the seconds it stays there.
Required scope
Room.CursorChatSent must be inside <Room.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { text: string }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/cursor-chat-sent.tsx
Room.CursorHintsscope
The pointers scrolled out of view, each as a chip on the edge nearest to it. Worked out ten times a second rather than every frame: a chip is a signpost, and a signpost that trembles along with the pointer it points at is harder to click.
Required scope
Room.CursorHints must be inside <Room.Root>.
Room.CursorHints must be inside <Room.CursorArea>.
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/cursor-hints.tsx
Room.CursorHintaction · <button>
A chip on the edge for a pointer out of view, pointing towards it. Pressing it follows them, which scrolls to where they are. `state.angle` is in radians, for an arrow to turn; `--peer` and `--peer-ink` are their colour and the ink that reads on it.
Required scope
Room.CursorHint must be inside <Room.CursorHints>.
Room.CursorHint must be inside <Room.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { angle: number; name: string }) => 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/cursor-hint.tsx
Room.Followaction · <button>
Follow this person — their pointer on a board, their tiles in a call — or stop. `aria-pressed` and `data-active` while following. Disabled on your own row: there is no following yourself, and a list that includes you stays one list.
Required scope
Room.Follow must be inside <Room.Peers>.
Room.Follow must be inside <Room.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
onUserClickCalled with the person pressed, as the view starts or stops following them. | (peer: Peer) => void | — |
childrenContent rendered inside this part. | ReactNode | ((state: { following: 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/follow.tsx
Room.Followinggate
You are following somebody: who, and their colour, for the frame that says so.
Required scope
Room.Following must be inside <Room.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { name: string; color: string }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/following.tsx
Room.StopFollowingaction · <button>
Take the view back.
Required scope
Room.StopFollowing 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/stop-following.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.Peersscope
Who is here, each in their own scope: you first, then everybody in the order they came. Peers are connections, so two tabs are two of them. Collapsing per person is a display decision and lives here rather than in the wire format, which is why it is a prop. Inside a `Room.Roster`, only the names matching its search are listed.
Required scope
Room.Peers must be inside <Room.Root>.
Props
7 documented props
| Prop | Type | Default |
|---|---|---|
perPersonOne row per person rather than per connection: two tabs become one entry. | boolean | false |
includeMeWhether you are in the list. A face pile usually wants you out of it. | boolean | true |
maxThe most items a pile may take, counting its "+N". Past it the pile shows one face fewer
and | number | — |
overflowOfOnly those a pile of this size leaves out: what its "+N" opens. | number | — |
limit@deprecated A hard cut with no room kept for the count; use | number | — |
groupWhat the items are grouped in. A Motion presence group here is what lets a face leave rather than vanish, without this part knowing anything about animation. | 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/peers.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.