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

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.

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

    Room.CursorArea props
    PropTypeDefault
    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/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

    Room.CursorSurface props
    PropTypeDefault
    children

    Content rendered inside this part.

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

    Room.Cursors props
    PropTypeDefault
    perPerson

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

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

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

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

    Room.CursorMessage props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { text: string }) => 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/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

    Room.CursorChat props
    PropTypeDefault
    children

    Content rendered inside this part.

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

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

    Room.CursorChatSent props
    PropTypeDefault
    children

    Content 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

    Room.CursorHints props
    PropTypeDefault
    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/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

    Room.CursorHint props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { angle: number; name: string }) => 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/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

    Room.Follow props
    PropTypeDefault
    onUserClick

    Called with the person pressed, as the view starts or stops following them.

    (peer: Peer) => void—
    children

    Content rendered inside this part.

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

    Room.Following props
    PropTypeDefault
    children

    Content 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

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

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

    Room.Peers props
    PropTypeDefault
    perPerson

    One row per person rather than per connection: two tabs become one entry.

    booleanfalse
    includeMe

    Whether you are in the list. A face pile usually wants you out of it.

    booleantrue
    max

    The most items a pile may take, counting its "+N". Past it the pile shows one face fewer and Room.MorePeers takes the last place, so the row keeps the width it was given.

    number—
    overflowOf

    Only 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 max.

    number—
    group

    What 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 }>—
    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/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.
    Read the setup guide →