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

Headless / Collections

Hierarchical records

Build collection hierarchy on your collection's schema, query and permissions.

8 public parts · 1examples

@slices.sh/collection/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

Collection.Root · Scope

Collection.RootThe root, and the parts that choose what is drawn under it: one of several views, the data a view needs, and one of two screens by width.

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/collection@^0.1.6

Import from @slices.sh/collection/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 collection.memory

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
  • Rows are lost when the server restarts, and live updates reach only watchers in the process that wrote. Move on with npx shadcn@latest add @slices/collection.mongodb (or -neon, -supabase): services/collection/*.config.ts and rules.ts are yours and stay.

Start small

Parts bring the behavior. Use their default elements, or supply your own with render.

example.tsx
"use client"import "@/services"import { Collection } from "@slices.sh/collection/headless"import type { CollectionSchema } from "@slices.sh/collection/services/types"export function Example({ schema }: { schema: CollectionSchema }) {  return (    <Collection.Root collection={schema.collection} schema={schema}>      <Collection.Hierarchy parent="parentId">        <Collection.Tree parent="parentId">          <Collection.Node>            <Collection.Expand>Expand</Collection.Expand>            <Collection.Cell render={<span />}>              <Collection.Value />            </Collection.Cell>          </Collection.Node>        </Collection.Tree>      </Collection.Hierarchy>    </Collection.Root>  )}

The schema and provider must support hierarchy. Set the parent field before rendering a tree.

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.

Collection.Hierarchyscope

A treegrid over a parent field: the read that nests records, the tree itself, a node per record on screen, the expander, the indent, moving a node in or out, a progress bar over its children, and the gate that says there is nothing to nest.

Required scope

Hierarchy requires Collection.Root.

Props

3 documented props

Collection.Hierarchy props
PropTypeDefault
parentRequired
string—
progressField
string—
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/collection/src/headless/hierarchy.tsx

Collection.Treecontent · <div>

The rows as a hierarchy somebody can walk: a `treegrid`, and the element that holds it. One tab stop for the whole tree, on a row. Up and down move between the rows on screen, Right opens a closed row and steps into an open one, Left closes it or goes to its parent, Home and End reach the ends, typing jumps to a name, and `*` opens a whole row of siblings. Enter or F2 steps into the row's own controls and Escape hands the tree back. `treegrid` rather than `tree`, because a row here carries a chevron, a checkbox and a delete button, and the tree role forbids interactive descendants outright — which is the same reading react-aria came to. The rows inside are `role="row"` and what they hold is `role="gridcell"`; focus stays on rows, so the cells are a shape a screen reader can read rather than places to be. Its own part rather than something `Root` does, because which field points upwards is the screen's to say: the same collection is a flat table on one page and a tree on another. → https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/

Required scope

Collection.Tree must be inside <Collection.Root>.

Tree surface requires a hierarchy.

Tree requires Collection.Root.

Collection.Tree requires Collection.Root.

Props

4 documented props · Native <div> attributes supported

Collection.Tree props
PropTypeDefault
selectable

The field a row names the one above it in, by the value its primary field holds.

booleanfalse
parentRequired
string—
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/collection/src/headless/tree.tsx

Collection.Nodecontent · <div>

One row of a tree, repeated over everything a reader can currently see. Flat in the DOM and deep only in what it says: the level, how many siblings it has and which of them it is are attributes, so a screen reader hears the shape without the markup having one, and the parts inside stay the parts a table uses. `aria-setsize` and `aria-posinset` are what the APG requires of a tree whose nodes arrive as they are opened, and they are what makes this render legal rather than merely convenient. The row is the tab stop — `role="row"` in a `treegrid`, so it may hold controls — and exactly one of them carries `tabIndex={0}`.

Required scope

Collection.Node must be inside <Collection.Tree>.

Collection.Node requires Collection.Root.

Props

3 documented props · Native <div> attributes supported

Collection.Node props
PropTypeDefault
openOnClick
booleanfalse
children

Content rendered inside this part.

ReactNode | ((state: NodeState) => 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/collection/src/headless/node.tsx

Collection.Expandaction · <button>

Opens and closes a node. Away on a node with nothing under it, which has nothing to open.

Required scope

Collection.Expand must be inside <Collection.Node>.

Collection.Expand must be inside <Collection.Tree>.

Props

2 documented props · Native <button> attributes supported

Collection.Expand props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { expanded: 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/collection/src/headless/expand.tsx

Collection.Depthcontent · <span>

The room a node's level takes before it starts. An element rather than a class, because how deep a row sits is data and a class list is not: a skin decides what the indent looks like, never how far in it goes.

Required scope

Collection.Depth must be inside <Collection.Node>.

Props

2 documented props · Native <span> attributes supported

Collection.Depth props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: { depth: number }) => 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/collection/src/headless/depth.tsx

Collection.Reparentaction · <button>

A treegrid over a parent field: the read that nests records, the tree itself, a node per record on screen, the expander, the indent, moving a node in or out, a progress bar over its children, and the gate that says there is nothing to nest.

Props

3 documented props · Native <button> attributes supported

Collection.Reparent props
PropTypeDefault
directionRequired
"indent" | "outdent"—
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/collection/src/headless/reparent.tsx

Collection.TreeProgresscontent · <progress>

A treegrid over a parent field: the read that nests records, the tree itself, a node per record on screen, the expander, the indent, moving a node in or out, a progress bar over its children, and the gate that says there is nothing to nest.

Required scope

TreeProgress requires Collection.Node.

Props

2 documented props · Native <progress> attributes supported

Collection.TreeProgress props
PropTypeDefault
children

Content rendered inside this part.

ReactNode | ((state: NodeProgress) => 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/collection/src/headless/tree-progress-bar.tsx

Collection.Unnestedgate

A treegrid over a parent field: the read that nests records, the tree itself, a node per record on screen, the expander, the indent, moving a node in or out, a progress bar over its children, and the gate that says there is nothing to nest.

Required scope

Collection.Unnested must be inside <Collection.Root>.

Props

1 documented props

Collection.Unnested 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/collection/src/headless/unnested.tsx

Collection.Rootscope

The root, and the parts that choose what is drawn under it: one of several views, the data a view needs, and one of two screens by width.

Props

25 documented props

Collection.Root props
PropTypeDefault
treeParent
string—
view
"table" | "list" | "grid" | "board" | "tree" | "detail"—
views
readonly CollectionViewValue[]—
cardSize
"small" | "medium" | "large"—
savedViews
readonly SavedView[]—
onSaveView
(view: SavedView) => void | Promise<void>—
groupBy
string | null—
onGroupByChange
(field: string | null) => void—
enabled

Left out or true: required, and a missing service throws. "optional": renders its children with no rows, and every part inside renders nothing. false: off. A capability name ("live") requires the service and that capability.

Enabled—
config

The collection config, "default" when left out. collection names one inside it.

string—
groupSumFields
readonly string[]—
cardSizing
booleanfalse
lanes
booleanfalse
collectionRequired
string—
schemaRequired
CollectionSchema—
density
"compact" | "comfortable" | "spacious"—
onDensityChange
(density: DensityValue) => void—
palette
booleanfalse
pageSize
number10
infinite
booleanfalse
live
booleanfalse
urlState

Whether the search, the sort and the filter travel in the address bar.

booleantrue
search

false when nothing on screen searches: no search applies, from the address or a saved view.

booleantrue
filters

false when nothing on screen filters: no filter applies, from the address or a saved view.

booleantrue
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/collection/src/headless/root.tsx

When something is missing

No service configuration
Install the collection 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 →