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

Headless / Realtime

Annotate together

Build shared drawing 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.Drawing>        <Room.DrawingSurface>          <Room.Strokes />        </Room.DrawingSurface>        <Room.DrawTool tool="pen">Pen</Room.DrawTool>        <Room.UndoStroke>Undo</Room.UndoStroke>        <Room.ClearMine>Clear my strokes</Room.ClearMine>      </Room.Drawing>    </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.Drawingscope

    The shared drawing, the thing this person is drawing with, and the drawing they are doing. Three concerns and three contexts: choosing a colour must not redraw the surface, and a stroke arriving must not redraw the toolbar. → docs/rules/no-wide-context.md

    Props

    1 documented props

    Room.Drawing 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/drawing-root.tsx

    Room.DrawingSurfacecontent · <svg>

    The surface, as an `<svg>`. A viewBox of 0–1 in both directions, matching the fractions strokes travel in, so the same drawing fits whatever size the element happens to be — and two people looking at different window sizes see the same picture rather than the same pixels. `touch-action: none` is what stops a phone scrolling the page instead of drawing on it. `role="application"` and a tab stop, because this is a control rather than a picture: it takes arrow keys, and a screen reader that passed them to its own reading cursor would leave a keyboard user with no way to draw anything at all. The label is the screen's to write. → hooks/realtime/use-drawing-input.ts

    Required scope

    Room.DrawingSurface must be inside <Room.Drawing>.

    Props

    2 documented props · Native <svg> attributes supported

    Room.DrawingSurface props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { tool: 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/drawing-surface.tsx

    Room.Strokesscope

    Every stroke on the surface, oldest underneath. Drawn here rather than handed out one scope at a time, because a stroke is a `<path>` and there is nothing about it a skin would change that `Room.DrawingSurface` cannot: the colour and the width belong to whoever drew it, not to the theme. `data-stroke` is how the eraser knows what it is over: a stroked path with no fill is hit only along the line itself, so the browser's own hit testing is the hit testing.

    Required scope

    Room.Strokes must be inside <Room.Drawing>.

    Props

    1 documented props

    Room.Strokes 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/strokes.tsx

    Room.DrawingCaretcontent · <circle>

    Where a keyboard is drawing from. Nothing at all until somebody uses one, because a crosshair on a surface nobody is navigating by keyboard is a mark with no meaning. Placed in the surface's own coordinates, so it belongs inside the `<svg>`. → docs/rules/capability-honesty.md

    Required scope

    Room.DrawingCaret must be inside <Room.Drawing>.

    Props

    2 documented props · Native <circle> attributes supported

    Room.DrawingCaret props
    PropTypeDefault
    children

    Content rendered inside this part.

    ReactNode | ((state: { drawing: 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/drawing-caret.tsx

    Room.DrawToolaction · <button>

    Draw, or erase. `data-state="selected"` rather than a conditional in the screen, so a toolbar is a row of these and never a branch. → docs/rules/states-gate-themselves.md

    Required scope

    Room.DrawTool must be inside <Room.Drawing>.

    Props

    3 documented props · Native <button> attributes supported

    Room.DrawTool props
    PropTypeDefault
    toolRequired
    "pen" | "eraser"—
    children

    Content rendered inside this part.

    ReactNode | ((state: { selected: 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/draw-tool.tsx

    Room.DrawColoraction · <button>

    One colour to draw in. The swatch is its own colour, which is data rather than a look.

    Required scope

    Room.DrawColor must be inside <Room.Drawing>.

    Props

    3 documented props · Native <button> attributes supported

    Room.DrawColor props
    PropTypeDefault
    valueRequired
    string—
    children

    Content rendered inside this part.

    ReactNode | ((state: { selected: 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/draw-color.tsx

    Room.DrawWidthaction · <button>

    One line thickness.

    Required scope

    Room.DrawWidth must be inside <Room.Drawing>.

    Props

    3 documented props · Native <button> attributes supported

    Room.DrawWidth props
    PropTypeDefault
    valueRequired
    number—
    children

    Content rendered inside this part.

    ReactNode | ((state: { selected: 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/draw-width.tsx

    Room.UndoStrokeaction · <button>

    Take your own last stroke back. Yours, and only yours: an undo that reached across a shared surface would be one person able to unpick another's work, which is a different feature and a worse one. Disabled when you have drawn nothing, because there is nothing to take back.

    Required scope

    Room.UndoStroke must be inside <Room.Drawing>.

    Room.UndoStroke must be inside <Room.Root>.

    Props

    2 documented props · Native <button> attributes supported

    Room.UndoStroke 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/undo-stroke.tsx

    Room.ClearMineaction · <button>

    Take back everything you drew, and nothing anybody else did. The safe half of clearing, and the one a person reaches for nine times out of ten: it destroys only your own work, so it needs no confirmation and no undo.

    Required scope

    Room.ClearMine must be inside <Room.Drawing>.

    Room.ClearMine must be inside <Room.Root>.

    Props

    2 documented props · Native <button> attributes supported

    Room.ClearMine 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/clear-mine.tsx

    Room.ClearDrawingaction · <button>

    Wipes the surface, for everybody. There is no undo across a shared document, so this destroys work somebody else may have spent an hour on and cannot get back. It is the one action in this family that a screen must put behind a confirmation — `Room.ClearMine` is the one that needs none, and is what most people actually want. Disabled while the surface is already empty.

    Required scope

    Room.ClearDrawing must be inside <Room.Drawing>.

    Props

    2 documented props · Native <button> attributes supported

    Room.ClearDrawing 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/clear-drawing.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

    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 →