Headless / Realtime
Collaborative canvas
Build collaborative whiteboard with the room's shared state and capability-aware controls.
32 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, Board } from "@slices.sh/realtime/headless"export function Example() { return ( <Room.Root room="studio"> <Board.Root> <Board.Tool tool="rect">Rectangle</Board.Tool> <Board.Tool tool="select">Select</Board.Tool> <Board.Surface> <Board.Canvas> <Board.Shapes> <Board.Shape /> </Board.Shapes> </Board.Canvas> </Board.Surface> <Board.Undo>Undo</Board.Undo> <Board.Redo>Redo</Board.Redo> <Board.Export>Export</Board.Export> </Board.Root> </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.
Board.Rootscope
A whiteboard in the room: an infinite surface of shapes everybody edits together, each person with their own view, tool, selection and history. `inks` says what each ink is on this screen and `outline` how a freehand line is shaped; both are looks, so both come from the screen. → hooks/realtime/board-look.ts Following somebody, from a face in a presence list or a `Room.Follow`, keeps their pointer in the middle of this person's view until this person takes the view back. → ./board-surface.tsx
Required scope
Board.Root must be inside <Room.Root>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
toolsThe tools this screen offers: the board starts on one of them and its keys pick no other, so no tool without a button can be in hand. Every tool when left out. | readonly BoardTool[] | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Also inherits Partial<BoardLook>. See TypeScript for the complete declaration.
Source: @slices.sh/realtime/src/headless/board-root.tsx
Board.Surfacecontent · <div>
The board itself: where the pointer draws, pans and selects, and where the keyboard picks a tool — only while it has focus, so a letter typed in a chat beside it is a letter. The camera travels as `--board-x`, `--board-y` and `--board-zoom`, so a grid drawn in CSS moves with the board without a render per frame; `data-state` names the tool, for a cursor. A press, a wheel turn or Escape on it ends following somebody.
Required scope
Board.Surface must be inside <Board.Root>.
Props
2 documented props · Native <div> 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/board-surface.tsx
Board.Canvascontent · <svg>
The drawing, in world units: an SVG whose one group the camera moves, so a pan is one attribute changing rather than every shape redrawn.
Required scope
Board.Canvas must be inside <Board.Root>.
Props
2 documented props · Native <svg> 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/board-canvas.tsx
Board.Shapesscope
Every shape on the board, bottom first, each in its own scope.
Required scope
Board.Shapes must be inside <Board.Root>.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
group | 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/board-shapes.tsx
Board.Shapecontent · <g>
One shape, drawn. `data-state` is its type, `data-active` while it is selected; what it looks like is its own data and the screen's inks.
Required scope
Board.Shape must be inside <Board.Shapes>.
Board.Shape must be inside <Board.Root>.
Props
2 documented props · Native <g> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { type: 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/board-shape.tsx
Board.Selectioncontent · <g>
The box around what is selected, with a handle on each corner and edge and one above it for turning — drawn in `currentColor` at the same size on screen however far the board is zoomed. The handles carry `data-handle` (`rotate` for the one above), which is how a drag on one resizes or turns rather than moves.
Required scope
Board.Selection must be inside <Board.Root>.
Props
2 documented props · Native <g> 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/board-selection.tsx
Board.TheirSelectionscontent · <g>
What everybody else has selected: a dashed outline in their colour, their name on its corner — the same colour as their pointer, so a glance says whose hand is on what. Drawn at the same size on screen however far the board is zoomed.
Required scope
Board.TheirSelections must be inside <Board.Root>.
Board.TheirSelections must be inside <Room.Root>.
Props
2 documented props · Native <g> 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/board-their-selections.tsx
Board.Marqueecontent · <rect>
The box a drag on empty board is drawing, in `currentColor`, while it is drawn.
Required scope
Board.Marquee must be inside <Board.Root>.
Props
2 documented props · Native <rect> 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/board-marquee.tsx
Board.Laserscontent · <g>
Everybody's laser: a line that fades from its tail, in `currentColor`, gone a second after the pointer stops. Nothing is written to the board.
Required scope
Board.Lasers must be inside <Board.Root>.
Props
2 documented props · Native <g> 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/board-lasers.tsx
Board.Cursorsscope
Everybody else's pointer on the board, each in the scope of the peer it belongs to.
Required scope
Board.Cursors must be inside <Board.Root>.
Board.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/board-cursors.tsx
Board.Cursorcontent · <div>
One collaborator's pointer, placed on screen where it is on the board, with that person's colour as `--peer` and the ink that reads on it as `--peer-ink`. Just after somebody is picked to follow it says `data-state="pinged"` for a moment, for a skin to ring. It glides on the same spring as `Room.Cursor`, in board units, and the camera is applied as it is drawn: a pan or a zoom carries every pointer at once, while only a pointer's own samples move it along the board. → ./use-pointer-spring.ts
Required scope
Board.Cursor must be inside <Board.Cursors>.
Board.Cursor must be inside <Board.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/board-cursor.tsx
Board.TextEditorinput · <textarea>
Typing into a shape: a text area laid exactly over it, at its size on screen. Every keystroke reaches the others as it is typed; leaving it — Escape, a click elsewhere — records one step of history, and a text left empty is taken away rather than left as an invisible shape somebody later cannot find.
Required scope
Board.TextEditor must be inside <Board.Root>.
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/board-text-editor.tsx
Board.Emptygate
Nothing on the board yet: where a first hint goes.
Required scope
Board.Empty must be inside <Board.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/board-empty.tsx
Board.Summarygate
What is on the board, in words, for somebody who cannot see it.
Required scope
Board.Summary must be inside <Board.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { shapes: number; selected: number }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/board-summary.tsx
Board.Toolaction · <button>
Picks one tool. `aria-pressed` and `data-active` while it is the one in hand.
Required scope
Board.Tool must be inside <Board.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
toolRequired | "select" | "hand" | "pen" | "highlighter" | "eraser" | "laser" | "rect" | "ellipse" | "arrow" | "line" | "text" | "note" | "measure" | — |
childrenContent rendered inside this part. | ReactNode | ((state: { active: 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/board-tool.tsx
Board.Stylinggate
A style would change something: a shape is selected, or the tool in hand leaves one.
Required scope
Board.Styling must be inside <Board.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/board-styling.tsx
Board.Styleaction · <button>
One value of one style — an ink, a size, a dash, a fill — as a button. `aria-pressed` while it is the value in use; an ink carries its colour as `--ink`, for its swatch.
Required scope
Board.Style must be inside <Board.Root>.
Props
4 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
choiceRequired | K | — |
valueRequired | ShapeStyle[K] | — |
childrenContent rendered inside this part. | ReactNode | ((state: { active: 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/board-style.tsx
Board.Sizecontent · <div>
The size of the next stroke, or of the selected shapes, as a slider with one stop for each size: arrows, Home and End step it, and pressing or dragging along it picks the nearest stop. The position travels as `--size-index` and `--size-at`, so a screen can draw a thumb and a filled track in CSS; the content function gets the stops, for drawing each one.
Props
3 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
labelsWhat each size is called, for a screen reader to say as the slider's value. | Record<InkSize, string> | — |
childrenContent rendered inside this part. | ReactNode | ((state: BoardSizeState) => 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/board-size.tsx
Board.SizePreviewcontent · <span>
What the size in use draws: its stroke width as `--size-dot`, in the ink in use as `--ink`. A screen makes a dot of it, which grows as the slider moves up.
Required scope
Board.SizePreview must be inside <Board.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/board-size-preview.tsx
Board.Opacityinput · <input>
How opaque, from a tenth to whole, as a slider.
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/board-opacity.tsx
Board.Selectedgate
Something is selected: what acts on a selection belongs here.
Required scope
Board.Selected must be inside <Board.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { count: number }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/realtime/src/headless/board-selected.tsx
Board.Deleteaction · <button>
Deletes what is selected, as one step you can undo.
Required scope
Board.Delete must be inside <Board.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/board-delete.tsx
Board.Arrangeaction · <button>
Moves what is selected up or down the pile: `to` is `front`, `forward`, `backward` or `back`. Unavailable with nothing selected. Everybody sees the new order, and it is not a step of history — undo takes back what was drawn, not where it lies.
Required scope
Board.Arrange must be inside <Board.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
toRequired | "front" | "forward" | "backward" | "back" | — |
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/board-arrange.tsx
Board.Undoaction · <button>
Takes back the last thing you did on the board — never anybody else's.
Required scope
Board.Undo must be inside <Board.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/board-undo.tsx
Board.Redoaction · <button>
Does again what you last took back.
Required scope
Board.Redo must be inside <Board.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/board-redo.tsx
Board.Zoomaction · <button>
Zooms the view, about its middle.
Required scope
Board.Zoom must be inside <Board.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
toRequiredIn, out, the whole drawing, or back to actual size. | "in" | "out" | "fit" | "actual" | — |
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/board-zoom.tsx
Board.ZoomLevelcontent · <span>
The zoom as a person reads it: "100%".
Required scope
Board.ZoomLevel must be inside <Board.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/board-zoom-level.tsx
Board.Minimapcontent · <svg>
The whole board small, with the part you are looking at outlined: press or drag on it to go somewhere else. Shapes are boxes in their own ink, which is all a map needs to be.
Required scope
Board.Minimap must be inside <Board.Root>.
Props
2 documented props · Native <svg> 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/board-minimap.tsx
Board.Exportaction · <button>
Saves the drawing as an SVG file: every shape, framed, with the colours the page is showing written into it — the inks are theme tokens, and a file opened elsewhere has no theme.
Required scope
Board.Export must be inside <Board.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
name | string | "board" |
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/board-export.tsx
Board.RulerToggleaction · <button>
Rulers on or off. `aria-pressed` while they are shown.
Required scope
Board.RulerToggle must be inside <Board.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { on: 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/board-ruler-toggle.tsx
Board.Rulercontent · <svg>
A ruler along one edge of the board, in the unit chosen, following the camera. Ticks and numbers are `currentColor`; nothing while rulers are off.
Required scope
Board.Ruler must be inside <Board.Root>.
Props
3 documented props · Native <svg> attributes supported
| Prop | Type | Default |
|---|---|---|
edgeRequired | "top" | "left" | — |
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/board-ruler.tsx
Board.Unitaction · <button>
Measure in one unit. `aria-pressed` while it is the one in use.
Required scope
Board.Unit must be inside <Board.Root>.
Props
3 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
unitRequired | "px" | "cm" | "in" | "m" | — |
childrenContent rendered inside this part. | ReactNode | ((state: { active: 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/board-unit.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.