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.
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.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
| 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/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
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { tool: 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/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
| 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/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
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { drawing: 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/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
| Prop | Type | Default |
|---|---|---|
toolRequired | "pen" | "eraser" | — |
childrenContent rendered inside this part. | ReactNode | ((state: { selected: 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/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
| Prop | Type | Default |
|---|---|---|
valueRequired | string | — |
childrenContent rendered inside this part. | ReactNode | ((state: { selected: 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/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
| Prop | Type | Default |
|---|---|---|
valueRequired | number | — |
childrenContent rendered inside this part. | ReactNode | ((state: { selected: 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/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
| 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/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
| 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/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
| 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/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
| 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.