# Hierarchical records

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

Import: @slices.sh/collection/headless

Public parts: 8

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/collection@^0.1.6
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/collection.md

## Anatomy

### Composition

```text
Collection.Root
  Collection.Hierarchy parent="parent"
    Collection.Tree
      Collection.Node
        Collection.Expand
        Collection.Reparent
        Collection.TreeProgress
        Collection.Depth
    Collection.Unnested
```

## Minimal composition

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

## Complete API reference

### Collection.Hierarchy

scope

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.

Import: @slices.sh/collection/headless

Hierarchy requires Collection.Root.

```tsx
RootProps & { parent: string; progressField?: string }
```

Source: @slices.sh/collection/src/headless/hierarchy.tsx

### Collection.Tree

content · Element: 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/

Import: @slices.sh/collection/headless

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

Tree surface requires a hierarchy.

Tree requires Collection.Root.

Collection.Tree requires Collection.Root.

```tsx
TreeProps

type TreeProps = PartProps<"div"> & {
  /** The field a row names the one above it in, by the value its primary field holds. */
  selectable?: boolean
  parent: string
}
```

Source: @slices.sh/collection/src/headless/tree.tsx

### Collection.Node

content · Element: 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}`.

Import: @slices.sh/collection/headless

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

Collection.Node requires Collection.Root.

```tsx
NodeProps

type NodeProps = PartProps<"div", NodeState> & { openOnClick?: boolean }

type NodeState = { depth: number; expandable: boolean; expanded: boolean; focused: boolean }
```

Source: @slices.sh/collection/src/headless/node.tsx

### Collection.Expand

action · Element: button

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

Import: @slices.sh/collection/headless

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

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

```tsx
PartProps<"button", { expanded: boolean }>
```

Source: @slices.sh/collection/src/headless/expand.tsx

### Collection.Depth

content · Element: 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.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"span", { depth: number }>
```

Source: @slices.sh/collection/src/headless/depth.tsx

### Collection.Reparent

action · Element: 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.

Import: @slices.sh/collection/headless

```tsx
PartProps<"button"> & { direction: ReparentDirection }
```

Source: @slices.sh/collection/src/headless/reparent.tsx

### Collection.TreeProgress

content · Element: 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.

Import: @slices.sh/collection/headless

TreeProgress requires Collection.Node.

```tsx
PartProps<"progress", NodeProgress>
```

Source: @slices.sh/collection/src/headless/tree-progress-bar.tsx

### Collection.Unnested

gate

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.

Import: @slices.sh/collection/headless

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

```tsx
GateProps
```

Source: @slices.sh/collection/src/headless/unnested.tsx

### Collection.Root

scope

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.

Import: @slices.sh/collection/headless

```tsx
CollectionRootProps

type CollectionRootProps = RootProps &
  CollectionViewOptions &
  CardSizeOptions &
  SavedViewOptions &
  GroupingOptions & {
    /**
     * 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?: Enabled
    /** The collection config, `"default"` when left out. `collection` names one inside it. */
    config?: string
    groupSumFields?: readonly string[]
    cardSizing?: boolean
    lanes?: boolean
    collection: string
    schema: CollectionSchema
    density?: DensityValue
    onDensityChange?: (density: DensityValue) => void
    palette?: boolean
    pageSize?: number
    infinite?: boolean
    live?: boolean
    /** Whether the search, the sort and the filter travel in the address bar. */
    urlState?: boolean
    /** `false` when nothing on screen searches: no search applies, from the address or a saved view. */
    search?: boolean
    /** `false` when nothing on screen filters: no filter applies, from the address or a saved view. */
    filters?: boolean
  }
```

Source: @slices.sh/collection/src/headless/root.tsx
