# Data table

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

Import: @slices.sh/collection/headless

Public parts: 76

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

Only for "One owner's records": `npm install @slices.sh/auth@^0.1.10`

### 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.ViewOption value="table"
  Collection.GroupBy
    Collection.GroupOption value="status"
    Collection.GroupNone
  Collection.WhenGroupable
  Collection.Search
  Collection.Loading
    Collection.SkeletonRow
  Collection.Refreshing
  Collection.Empty
    Collection.LoadSampleRows
  Collection.Error
  Collection.WriteError
  Collection.MissingConfiguration
  Collection.SavedViews.Root
    Collection.SavedViews.WhenAvailable
      Collection.SavedViews.Select
        Collection.SavedViews.Option
      Collection.SavedViews.Unsaved
      Collection.SavedViews.WhenWritable
        Collection.SavedViews.Reset
        Collection.SavedViews.Name
        Collection.SavedViews.Save
        Collection.SavedViews.Error
  Collection.Display.Root
    Collection.Display.Trigger
    Collection.Display.Popup
      Collection.Display.WhenSortable
        Collection.Display.SortField
        Collection.Display.ClearSort
      Collection.Density
      Collection.WhenCardSizing
        Collection.CardSize
      Collection.ColumnToggle
  Collection.Group.Root
    Collection.Group.Heading
      Collection.Group.Label
    Collection.Group.Cell
      Collection.Group.Sums
  CollectionToolbar.Root
    CollectionToolbar.Trigger
    CollectionToolbar.Content
  CollectionPointer.Root
    Collection.ViewData
      Collection.Layout mode="wide"
        Collection.View name="table"
          Collection.Viewport
          Collection.Spacer
          Collection.Grid
            Collection.Column
              Collection.ColumnLabel
              Collection.SortTrigger
              Collection.Unsortable
            Collection.SpanningCell
            Collection.RowSurface provided internally by Grid
              Collection.Row
                Collection.Cell field="name"
                  Collection.Value
                CollectionPointer.Row
          Collection.PageRange
          Collection.PageNumbers
            Collection.PageNumber
            Collection.PageGap
          Collection.PrevPage
          Collection.NextPage
          Collection.LoadMore
          Collection.Sentinel
          Collection.Exhausted
          Collection.LoadingMore
      Collection.Layout mode="narrow"
```

## A table

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
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.Grid render={<table />}>
        <tbody>
          <Collection.Row render={<tr />}>
            <Collection.Cell render={<td />}>
              <Collection.Value />
            </Collection.Cell>
          </Collection.Row>
        </tbody>
      </Collection.Grid>
      <Collection.Empty>No projects yet.</Collection.Empty>
    </Collection.Root>
  )
}

```

The schema below, saved under services/collection/schemas/, defines the collection and its columns. Row and Cell repeat for you.

### services/collection/schemas/projects.ts

```tsx
import { defineCollection } from "@slices.sh/collection/services/define-collection"

export const projects = defineCollection({
  collection: "projects",
  title: "Projects",
  singular: "Project",
  fields: [
    { name: "name", label: "Name", type: "string", example: "Website redesign", primary: true, searchable: true, sortable: true, filterable: false },
    { name: "status", label: "Status", type: "enum", options: ["todo", "doing", "done"], example: "doing", primary: false, searchable: false, sortable: true, filterable: true },
    { name: "due", label: "Due", type: "date", example: "2026-11-01", primary: false, searchable: false, sortable: true, filterable: true },
  ],
})

```

The schema is the registration. The services/collection/default.config.ts a collection setup writes collects every file in services/collection/schemas/, so there is nothing to add to it. A config you wrote yourself lists it: collection({ provider, collections: [projects] }).

### app/page.tsx

```tsx
import { Example } from "@/components/example"
import { projects } from "@/services/collection/schemas/projects"

export default function Page() {
  return <Example schema={projects} />
}

```

Hands the schema to the example, which reads its columns from it.

## Search & paginate

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
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}
      pageSize={10}
    >
      <Collection.Search aria-label="Search projects" />
      <Collection.Row>
        <Collection.Cell>
          <Collection.Value />
        </Collection.Cell>
      </Collection.Row>
      <Collection.PrevPage>Previous</Collection.PrevPage>
      <Collection.NextPage>Next</Collection.NextPage>
    </Collection.Root>
  )
}

```

Use the same registered collection. The controls manage their query and disabled states.

### services/collection/schemas/projects.ts

```tsx
import { defineCollection } from "@slices.sh/collection/services/define-collection"

export const projects = defineCollection({
  collection: "projects",
  title: "Projects",
  singular: "Project",
  fields: [
    { name: "name", label: "Name", type: "string", example: "Website redesign", primary: true, searchable: true, sortable: true, filterable: false },
    { name: "status", label: "Status", type: "enum", options: ["todo", "doing", "done"], example: "doing", primary: false, searchable: false, sortable: true, filterable: true },
    { name: "due", label: "Due", type: "date", example: "2026-11-01", primary: false, searchable: false, sortable: true, filterable: true },
  ],
})

```

The schema is the registration. The services/collection/default.config.ts a collection setup writes collects every file in services/collection/schemas/, so there is nothing to add to it. A config you wrote yourself lists it: collection({ provider, collections: [projects] }).

### app/page.tsx

```tsx
import { Example } from "@/components/example"
import { projects } from "@/services/collection/schemas/projects"

export default function Page() {
  return <Example schema={projects} />
}

```

Hands the schema to the example, which reads its columns from it.

## One owner's records

```tsx
"use client"

// services/index.ts comes from https://slices.sh/r/services.json (or any provider setup)
import "@/services"
import { Collection } from "@slices.sh/collection/headless"
import { Principals } from "@slices.sh/auth/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.Owner.Root>
        <Collection.Owner.Select aria-label="Whose projects">
          <Collection.Owner.Any>Everything I can see</Collection.Owner.Any>
          <Principals.Known>
            <Principals.PersonalOption>Mine</Principals.PersonalOption>
            <Principals.OrganizationOption />
          </Principals.Known>
        </Collection.Owner.Select>
        <Collection.Owner.WhenChosen>
          {({ owner }) => <p>Showing {owner}</p>}
        </Collection.Owner.WhenChosen>
        <Collection.Owner.WhenAny>
          <p>Showing every project you can read</p>
        </Collection.Owner.WhenAny>
        <Collection.Owner.Rows>
          <Collection.Row>
            <Collection.Cell>
              <Collection.Value />
            </Collection.Cell>
          </Collection.Row>
        </Collection.Owner.Rows>
      </Collection.Owner.Root>
    </Collection.Root>
  )
}

```

Needs @slices.sh/auth: a record's owner is a person or an organization of the signed-in account, and Principals lists them. Give the schema an owner field holding a principal (user:<id>, org:<id>). Rows narrows every read below it to the chosen owner; the server still decides what each viewer may read.

### services/collection/schemas/projects.ts

```tsx
import { defineCollection } from "@slices.sh/collection/services/define-collection"

export const projects = defineCollection({
  collection: "projects",
  title: "Projects",
  singular: "Project",
  fields: [
    { name: "name", label: "Name", type: "string", example: "Website redesign", primary: true, searchable: true, sortable: true, filterable: false },
    { name: "status", label: "Status", type: "enum", options: ["todo", "doing", "done"], example: "doing", primary: false, searchable: false, sortable: true, filterable: true },
    { name: "due", label: "Due", type: "date", example: "2026-11-01", primary: false, searchable: false, sortable: true, filterable: true },
  ],
})

```

The schema is the registration. The services/collection/default.config.ts a collection setup writes collects every file in services/collection/schemas/, so there is nothing to add to it. A config you wrote yourself lists it: collection({ provider, collections: [projects] }).

### app/page.tsx

```tsx
import { Example } from "@/components/example"
import { projects } from "@/services/collection/schemas/projects"

export default function Page() {
  return <Example schema={projects} />
}

```

Hands the schema to the example, which reads its columns from it.

## Saved views

```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.SavedViews.Root>
        <Collection.SavedViews.Select>
          <Collection.SavedViews.Option />
        </Collection.SavedViews.Select>
        <Collection.SavedViews.WhenWritable>
          <Collection.SavedViews.Name aria-label="View name" />
          <Collection.SavedViews.Save>Save view</Collection.SavedViews.Save>
        </Collection.SavedViews.WhenWritable>
        <Collection.SavedViews.Error />
      </Collection.SavedViews.Root>
    </Collection.Root>
  )
}

```

Supply a registered collection schema. Parts use its fields, permissions and provider capabilities.

## Display options

```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.Display.Root>
        <Collection.Display.Trigger>
          Display options
        </Collection.Display.Trigger>
        <Collection.Display.Popup label="Display options">
          <Collection.Display.WhenSortable>
            <Collection.Display.SortField />
            <Collection.Display.ClearSort>
              Clear sort
            </Collection.Display.ClearSort>
          </Collection.Display.WhenSortable>
        </Collection.Display.Popup>
      </Collection.Display.Root>
    </Collection.Root>
  )
}

```

Supply a registered collection schema. Parts use its fields, permissions and provider capabilities.

## Complete API reference

### 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

### Collection.Layout

content

One of two screens, by how much room there is.

A gate rather than a breakpoint class, because a phone does not want the same *elements* as
a desktop with smaller text: eight columns at 375px is a sideways scroll nobody asked for,
and the answer is cards. A class can hide a table; only a gate can stop building one.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<null> & { mode: "wide" | "narrow" }
```

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

### Collection.View

gate

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

Collection.View requires Collection.Root.

```tsx
GateProps & { name: CollectionViewValue | readonly CollectionViewValue[] }
```

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

### Collection.ViewData

scope

Presentation-specific reads stay below the common query, selection and open-record root.

Import: @slices.sh/collection/headless

ViewData requires Collection.Root.

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

Source: @slices.sh/collection/src/headless/view-data.tsx

### CollectionPointer.Root

scope

The isolated pointer engine: a 5 px or 200 ms activation with a mouse, a 250 ms press on touch, and a drop that becomes one move write (group, and position when orderBy is numeric). A refused write rolls the card back and returns focus to it.

Import: @slices.sh/collection/headless/collection-pointer

Pointer.Root requires Collection.Root.

```tsx
RootProps
```

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

### Collection.Grid

content · Element: table

Row focus is the default. Cell mode preserves the existing APG grid keyboard.

Import: @slices.sh/collection/headless

```tsx
PartProps<"table"> & { focus?: "row" | "cell"; selectable?: boolean }
```

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

### Collection.Viewport

content · Element: div

The box a long list scrolls inside, and the only thing in the family that decides a row is
not worth drawing.

Without it every loaded row is in the DOM: a board of five thousand cards is five thousand
card subtrees, and a `dragover` measured every one of them. With it the DOM holds a window
and two empty spacers, and `pageSize` stops being a limit on what a browser can take.

Inside a lane it counts that lane's cards rather than the board's, so each lane scrolls on
its own — which is what a board is.

Rows are taken to be one height, declared as `rowHeight`: a list of cards whose height
depends on their content should not virtualize, and saying so is cheaper than measuring
every row and being wrong about the ones not drawn yet.

Import: @slices.sh/collection/headless

Collection.Viewport requires Collection.Root.

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

```tsx
ViewportProps

type ViewportProps = PartProps<"div"> & {
  /** Disable for an intrinsically hierarchical view or a custom renderer without Group parts. */
  grouped?: boolean
  /** What one row is worth in pixels, which only the screen knows. */
  rowHeight?: number | Readonly<Record<DensityValue, number>>
  /** Under this many rows the list is drawn whole. */
  threshold?: number
}
```

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

### Collection.Row

content · Element: tr

The row surface of a list, grid or board, the row repeated per record, the cell repeated per field (all, primary, media or details, or one field by name) and the value formatted by field type. Value renders nothing when the field is empty.

Import: @slices.sh/collection/headless

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

Collection.Row requires Collection.Root.

```tsx
RowProps

type RowProps = PartProps<"tr", { selected: boolean; dragging: boolean }> & { openOnClick?: boolean }
```

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

### Collection.RowSurface

content · Element: div

A grid whose records, rather than cells, hold its roving tab stop.

Import: @slices.sh/collection/headless

Collection.RowSurface requires Collection.Root.

```tsx
PartProps<"div"> & {
  defaultTag?: "div" | "table"
  header?: boolean
  selectable?: boolean
  layout?: "linear" | "grid" | "board"
}
```

Source: @slices.sh/collection/src/headless/row-surface.tsx

### CollectionPointer.Row

content · Element: div

Lane makes a lane a drop target, empty lanes included. Row registers a card as sortable within its lane and keeps the row's role, tabIndex, aria and data attributes, so the engine never replaces selection semantics.

Import: @slices.sh/collection/headless/collection-pointer

Capability: order (positions within a lane need a numeric orderBy)

Pointer.Row requires Collection.Row.

Pointer.Row requires Collection.Lane.

Pointer.Row requires Collection.Root.

```tsx
PartProps<"div">
```

Source: @slices.sh/collection/src/headless/collection-pointer/row.tsx

### Collection.Cell

content · Element: td

The row surface of a list, grid or board, the row repeated per record, the cell repeated per field (all, primary, media or details, or one field by name) and the value formatted by field type. Value renders nothing when the field is empty.

Import: @slices.sh/collection/headless

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

```tsx
CellProps

type CellProps = PartProps<"td", { field: SchemaField }> & FieldChoice
```

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

### Collection.Value

content · Element: img

The row surface of a list, grid or board, the row repeated per record, the cell repeated per field (all, primary, media or details, or one field by name) and the value formatted by field type. Value renders nothing when the field is empty.

Import: @slices.sh/collection/headless

Collection.Value must be inside <Collection.Row>.

Collection.Value must be inside a part that repeats over fields, like <Collection.Cell>.

```tsx
PartProps<null, ValueState>

type ValueState = { value: unknown; text: string }
```

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

### Collection.Column

content · Element: th

A grid that holds one tab stop, headers that repeat per field with sort triggers (Shift adds a key), a switch per column, a cell spanning the table and skeleton rows for the first load only.

Import: @slices.sh/collection/headless

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

```tsx
ColumnProps

type ColumnProps = PartProps<"th", ColumnState> & FieldChoice

type ColumnState = { field: SchemaField; sort: "asc" | "desc" | null }
```

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

### Collection.ColumnLabel

content

A grid that holds one tab stop, headers that repeat per field with sort triggers (Shift adds a key), a switch per column, a cell spanning the table and skeleton rows for the first load only.

Import: @slices.sh/collection/headless

Collection.ColumnLabel must be inside a part that repeats over fields, like <Collection.Column>.

```tsx
PartProps<null>
```

Source: @slices.sh/collection/src/headless/column-label.tsx

### Collection.Unsortable

gate

A grid that holds one tab stop, headers that repeat per field with sort triggers (Shift adds a key), a switch per column, a cell spanning the table and skeleton rows for the first load only.

Import: @slices.sh/collection/headless

Collection.Unsortable must be inside <Collection.Column>.

```tsx
GateProps
```

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

### Collection.SortTrigger

action · Element: button

Sorts by the field it is inside: unsorted, ascending, descending, unsorted again.

Shift-clicking adds a key instead of replacing the one before it, which is what `useSort`'s
`additive` was written for and what nothing ever passed — so a table that could sort by
status and then by amount could only ever be asked for one of them. Plain clicking still
replaces, because a table that piled up keys would answer a question nobody asked by the
third click.

`rank` is how a pattern says which key is which: state rather than a data attribute, because
the shared attribute vocabulary is not this family's to widen. A screen reader hears the
direction from `aria-sort` on the header cell; the order is a number beside the icon, which
is what every grid does with it.

Import: @slices.sh/collection/headless

Collection.SortTrigger must be inside <Collection.Column>.

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

```tsx
PartProps<"button", SortState>

type SortState = {
  direction: "asc" | "desc" | null
  /** Which key this field is, when more than one field is sorted. `null` when it is the only one. */
  rank: number | null
}
```

Source: @slices.sh/collection/src/headless/sort-trigger.tsx

### Collection.Search

input · Element: input

A search box over the collection's searchable fields, with data-pending until the rows answer. It renders nothing when no field is searchable or the provider cannot search.

Import: @slices.sh/collection/headless

Capability: search

Collection.Search requires Collection.Root.

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

```tsx
PartProps<"input">
```

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

### Collection.PageRange

content

Which rows of the collection are on screen: "11–20 of 312".

Here from the first render, before there is a range to say. Patterns draw this in an
`<output>`, which is a `status` region, and a live region only announces what arrives after
it is mounted — so a range that appeared together with its own first text announced nothing,
and a query that matched no rows announced nothing ever, because the element was never
there to say so. Empty until there is a page, like `Collection.ResultCount`.

`to` is 0 while no page has arrived, which is what a caller branches on: "0–0 of 0" is a
claim about an empty collection, and a page in flight is not that.

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

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

```tsx
PartProps<null, PageRangeState>

type PageRangeState = { from: number; to: number; total: number | null }
```

Source: @slices.sh/collection/src/headless/page-range.tsx

### Collection.PageNumbers

scope

A row of page numbers, each in its own scope.

Renders nothing at all when the backend did not count the rows: `pages` is `null` then,
not a guess, and a pager numbered from a guess sends people to pages that are not there.
Previous and Next still work in that case, which is why a numbered pager keeps them.
→ docs/rules/capability-honesty.md

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

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

```tsx
RootProps
```

Source: @slices.sh/collection/src/headless/page-numbers.tsx

### Collection.PageNumber

action · Element: button

One page, to go to.

A button rather than a link, because the page is client state and never an address here.
`data-state` is `current` or `other`, which is what a skin marks the one you are on by —
and `aria-current` says the same thing to a screen reader, which cannot see the mark.

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

Collection.PageNumber must be inside <Collection.PageNumbers>.

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

```tsx
PartProps<"button", { page: number; current: boolean }>
```

Source: @slices.sh/collection/src/headless/page-number.tsx

### Collection.PageGap

content · Element: span

Where a numbered pager left pages out.

Hidden from a screen reader: it is a mark that some numbers are missing, which is only
information to somebody who can see the row it is missing from. Reading "ellipsis"
between two page numbers says nothing.

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

Collection.PageGap must be inside <Collection.PageNumbers>.

```tsx
PartProps<"span">
```

Source: @slices.sh/collection/src/headless/page-gap.tsx

### Collection.PrevPage

action · Element: button

The range on screen, previous and next, and page numbers between them. PageNumbers renders nothing when the provider did not count, so previous and next still work; PageRange says its total as null.

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

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

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

Source: @slices.sh/collection/src/headless/prev-page.tsx

### Collection.NextPage

action · Element: button

The range on screen, previous and next, and page numbers between them. PageNumbers renders nothing when the provider did not count, so previous and next still work; PageRange says its total as null.

Import: @slices.sh/collection/headless

Capability: countMatching (PageNumbers, PageRange total)

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

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

Source: @slices.sh/collection/src/headless/next-page.tsx

### Collection.ViewOption

action · Element: button

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

Collection.ViewOption requires Collection.Root.

```tsx
PartProps<"button", { active: boolean }> & { value: CollectionViewValue }
```

Source: @slices.sh/collection/src/headless/view-option.tsx

### Collection.Display.Trigger

action · Element: button

The root, its trigger and its popup.

Import: @slices.sh/collection/headless

```tsx
PartProps<"button">
```

Alternate import: Display.Trigger from @slices.sh/collection/headless/display

Source: @slices.sh/collection/src/headless/display/trigger.tsx

### Collection.Display.Root

scope

The root, its trigger and its popup.

Import: @slices.sh/collection/headless

```tsx
RootProps
```

Alternate import: Display.Root from @slices.sh/collection/headless/display

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

### Collection.Display.Popup

content · Element: div

The root, its trigger and its popup.

Import: @slices.sh/collection/headless

Display.Popup requires Display.Root.

```tsx
PartProps<"div"> & {
  label: string
  /** The popover the options open in, which the pattern styles; it aligns to its trigger's end unless told otherwise. */
  popup?: ReactElement<ComponentProps<typeof MotionPopover>>
}
```

Alternate import: Display.Popup from @slices.sh/collection/headless/display

Source: @slices.sh/collection/src/headless/display/popup.tsx

### Collection.Density

input · Element: select

Row density, and card size where the root offers it.

Import: @slices.sh/collection/headless

Collection.Density requires Collection.Root.

```tsx
PartProps<"select">
```

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

### Collection.CardSize

input · Element: select

Row density, and card size where the root offers it.

Import: @slices.sh/collection/headless

Collection.CardSize requires Collection.Root.

```tsx
PartProps<"select">
```

Source: @slices.sh/collection/src/headless/card-size.tsx

### Collection.WhenCardSizing

gate

Row density, and card size where the root offers it.

Import: @slices.sh/collection/headless

Collection.WhenCardSizing requires Collection.Root.

```tsx
GateProps
```

Source: @slices.sh/collection/src/headless/when-card-sizing.tsx

### Collection.ColumnToggle

action · Element: button

One switch per column, for a view-options menu.

`useColumns` was written, unit-tested and imported by nothing: hiding a column needed a
context, a part and a param in seven `item.json`s, and the friction of doing that seven
times is why nobody did. This is the part. It also lifts the install-time cap of eight
fields, which existed because a wide table with no way to narrow it is unusable.

Repeats over the columns in their own order rather than the schema's, so a reorder shows.
The field is in scope inside, which is what makes `Collection.ColumnLabel` work here.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"button", ToggleState>

type ToggleState = { field: SchemaField; visible: boolean }
```

Source: @slices.sh/collection/src/headless/column-toggle.tsx

### Collection.Display.WhenSortable

gate

The gate that hides sorting when a host turns it off, a toggle per sortable field (click cycles, Shift adds a key) and clearing the sort.

Import: @slices.sh/collection/headless

Display.WhenSortable requires Collection.Root.

```tsx
GateProps & { enabled?: boolean }
```

Alternate import: Display.WhenSortable from @slices.sh/collection/headless/display

Source: @slices.sh/collection/src/headless/display/when-sortable.tsx

### Collection.Display.SortField

action · Element: button

The gate that hides sorting when a host turns it off, a toggle per sortable field (click cycles, Shift adds a key) and clearing the sort.

Import: @slices.sh/collection/headless

Display.SortField requires Collection.Root.

```tsx
PartProps<"button", SortFieldState>

type SortFieldState = { field: SchemaField; direction: "asc" | "desc" | null; rank: number | null }
```

Alternate import: Display.SortField from @slices.sh/collection/headless/display

Source: @slices.sh/collection/src/headless/display/sort-field.tsx

### Collection.Display.ClearSort

action · Element: button

The gate that hides sorting when a host turns it off, a toggle per sortable field (click cycles, Shift adds a key) and clearing the sort.

Import: @slices.sh/collection/headless

Display.ClearSort requires Collection.Root.

```tsx
PartProps<"button">
```

Alternate import: Display.ClearSort from @slices.sh/collection/headless/display

Source: @slices.sh/collection/src/headless/display/clear-sort.tsx

### Collection.SavedViews.Root

scope

The root, the gate around everything (renders while views exist or can be saved) and the gate around writing (renders only with onSaveView).

Import: @slices.sh/collection/headless

SavedViews.Root requires Collection.Root.

```tsx
RootProps & SavedViewOptions
```

Alternate import: SavedViews.Root from @slices.sh/collection/headless/saved-views

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

### Collection.SavedViews.WhenAvailable

gate

The root, the gate around everything (renders while views exist or can be saved) and the gate around writing (renders only with onSaveView).

Import: @slices.sh/collection/headless

SavedViews.WhenAvailable requires Collection.Root.

```tsx
GateProps
```

Alternate import: SavedViews.WhenAvailable from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/when-available.tsx

### Collection.SavedViews.Select

input · Element: select

A select of the saved views and an option per view, the marker of unsaved changes, and reset to the selected view.

Import: @slices.sh/collection/headless

SavedViews.Select requires Collection.Root.

```tsx
PartProps<"select">
```

Alternate import: SavedViews.Select from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/select.tsx

### Collection.SavedViews.Option

content · Element: option

A select of the saved views and an option per view, the marker of unsaved changes, and reset to the selected view.

Import: @slices.sh/collection/headless

SavedViews.Option requires Collection.Root.

```tsx
PartProps<"option", { view: SavedView }>
```

Alternate import: SavedViews.Option from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/option.tsx

### Collection.SavedViews.Unsaved

content · Element: span

A select of the saved views and an option per view, the marker of unsaved changes, and reset to the selected view.

Import: @slices.sh/collection/headless

SavedViews.Unsaved requires Collection.Root.

```tsx
PartProps<"span">
```

Alternate import: SavedViews.Unsaved from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/unsaved.tsx

### Collection.SavedViews.WhenWritable

gate

The root, the gate around everything (renders while views exist or can be saved) and the gate around writing (renders only with onSaveView).

Import: @slices.sh/collection/headless

SavedViews.WhenWritable requires Collection.Root.

```tsx
GateProps
```

Alternate import: SavedViews.WhenWritable from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/when-writable.tsx

### Collection.SavedViews.Name

input · Element: input

The name of a new view, Save (or Save create for a new id) with { pending }, and the refusal, which keeps the old baseline.

Import: @slices.sh/collection/headless

SavedViews.Name requires Collection.Root.

```tsx
PartProps<"input">
```

Alternate import: SavedViews.Name from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/name.tsx

### Collection.SavedViews.Save

action · Element: button

The name of a new view, Save (or Save create for a new id) with { pending }, and the refusal, which keeps the old baseline.

Import: @slices.sh/collection/headless

SavedViews.Save requires Collection.Root.

```tsx
PartProps<"button", { pending: boolean }> & { create?: boolean }
```

Alternate import: SavedViews.Save from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/save.tsx

### Collection.SavedViews.Reset

action · Element: button

A select of the saved views and an option per view, the marker of unsaved changes, and reset to the selected view.

Import: @slices.sh/collection/headless

SavedViews.Reset requires Collection.Root.

```tsx
PartProps<"button">
```

Alternate import: SavedViews.Reset from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/reset.tsx

### Collection.SavedViews.Error

content · Element: p

The name of a new view, Save (or Save create for a new id) with { pending }, and the refusal, which keeps the old baseline.

Import: @slices.sh/collection/headless

SavedViews.Error requires Collection.Root.

```tsx
PartProps<"p", { message: string }>
```

Alternate import: SavedViews.Error from @slices.sh/collection/headless/saved-views

Source: @slices.sh/collection/src/headless/saved-views/error.tsx

### Collection.WhenGroupable

gate

Choosing the enum field rows are grouped by. WhenGroupable hides the selector when no field can group.

Import: @slices.sh/collection/headless

Collection.WhenGroupable requires Collection.Root.

```tsx
GateProps & { enabled?: boolean }
```

Source: @slices.sh/collection/src/headless/when-groupable.tsx

### Collection.GroupBy

input · Element: select

Choosing the enum field rows are grouped by. WhenGroupable hides the selector when no field can group.

Import: @slices.sh/collection/headless

Collection.GroupBy requires Collection.Root.

```tsx
PartProps<"select">
```

Source: @slices.sh/collection/src/headless/group-by.tsx

### Collection.GroupOption

content · Element: option

Choosing the enum field rows are grouped by. WhenGroupable hides the selector when no field can group.

Import: @slices.sh/collection/headless

Collection.GroupOption requires Collection.Root.

```tsx
PartProps<"option", { field: SchemaField }>
```

Source: @slices.sh/collection/src/headless/group-option.tsx

### Collection.GroupNone

content · Element: option

Choosing the enum field rows are grouped by. WhenGroupable hides the selector when no field can group.

Import: @slices.sh/collection/headless

Collection.GroupNone requires Collection.Root.

```tsx
PartProps<"option">
```

Source: @slices.sh/collection/src/headless/group-none.tsx

### Collection.Group.Root

scope

The root that repeats a section per group, its heading, the group's label and the heading's table cell.

Import: @slices.sh/collection/headless

Group.Root requires Collection.Root.

```tsx
PartProps<null>
```

Alternate import: Group.Root from @slices.sh/collection/headless/group

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

### Collection.Group.Heading

content · Element: div

The root that repeats a section per group, its heading, the group's label and the heading's table cell.

Import: @slices.sh/collection/headless

Group.Heading requires Group.Root.

Group.Heading requires Collection.Root.

```tsx
PartProps<"div"> & { table?: boolean }
```

Alternate import: Group.Heading from @slices.sh/collection/headless/group

Source: @slices.sh/collection/src/headless/group/heading.tsx

### Collection.Group.Label

content

The root that repeats a section per group, its heading, the group's label and the heading's table cell.

Import: @slices.sh/collection/headless

Group.Label requires Group.Root.

Group.Label requires Collection.Root.

```tsx
PartProps<null, { value: string | null; count: number | null }>
```

Alternate import: Group.Label from @slices.sh/collection/headless/group

Source: @slices.sh/collection/src/headless/group/label.tsx

### Collection.Group.Cell

content · Element: td

The root that repeats a section per group, its heading, the group's label and the heading's table cell.

Import: @slices.sh/collection/headless

Group.Cell requires Collection.Root.

```tsx
PartProps<"td"> & { selectionColumn?: boolean; actionsColumn?: boolean }
```

Alternate import: Group.Cell from @slices.sh/collection/headless/group

Source: @slices.sh/collection/src/headless/group/cell.tsx

### Collection.Group.Sums

content · Element: span

The group's sums over groupSumFields. A sum the provider cannot compute is null, never zero.

Import: @slices.sh/collection/headless

Capability: groupSums

Group.Sums requires Collection.Root.

Group.Sums requires Group.Root.

```tsx
PartProps<"span", SumState>

type SumState = { field: SchemaField; value: number; text: string }
```

Alternate import: Group.Sums from @slices.sh/collection/headless/group

Source: @slices.sh/collection/src/headless/group/sums.tsx

### CollectionToolbar.Root

scope

The root; the trigger, which renders only on a compact screen; and the content, a labelled group inline on a desktop and a popover with native focus, Escape and motion on a compact screen. A custom render element cannot remove those.

Import: @slices.sh/collection/headless/collection-toolbar

```tsx
RootProps
```

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

### CollectionToolbar.Trigger

action · Element: button

The root; the trigger, which renders only on a compact screen; and the content, a labelled group inline on a desktop and a popover with native focus, Escape and motion on a compact screen. A custom render element cannot remove those.

Import: @slices.sh/collection/headless/collection-toolbar

CollectionToolbar.Trigger requires CollectionToolbar.Root.

```tsx
PartProps<"button">
```

Source: @slices.sh/collection/src/headless/collection-toolbar/trigger.tsx

### CollectionToolbar.Content

content · Element: div

A custom inner renderer cannot remove the popup's native focus, Escape or Motion lifetime.

Import: @slices.sh/collection/headless/collection-toolbar

CollectionToolbar.Content requires CollectionToolbar.Root.

```tsx
PartProps<"div"> & {
  label: string
  /** The popover the controls fold into on a narrow screen, which the pattern styles. */
  popup?: ReactElement<ComponentProps<typeof MotionPopover>>
}
```

Source: @slices.sh/collection/src/headless/collection-toolbar/content.tsx

### Collection.Loading

gate

The first load, a refresh with the old rows still on screen, nothing to show (and whether search or filters caused it), the read refused, the last write refused, and a server that answers config-missing at runtime.

Import: @slices.sh/collection/headless

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

```tsx
GateProps
```

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

### Collection.SkeletonRow

content · Element: tr

A grid that holds one tab stop, headers that repeat per field with sort triggers (Shift adds a key), a switch per column, a cell spanning the table and skeleton rows for the first load only.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"tr">
```

Source: @slices.sh/collection/src/headless/skeleton-row.tsx

### Collection.SpanningCell

content · Element: td

A grid that holds one tab stop, headers that repeat per field with sort triggers (Shift adds a key), a switch per column, a cell spanning the table and skeleton rows for the first load only.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"td">
```

Source: @slices.sh/collection/src/headless/spanning-cell.tsx

### Collection.Empty

gate

The first load, a refresh with the old rows still on screen, nothing to show (and whether search or filters caused it), the read refused, the last write refused, and a server that answers config-missing at runtime.

Import: @slices.sh/collection/headless

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

```tsx
GateProps<{ filtered: boolean }>
```

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

### Collection.Error

gate

The first load, a refresh with the old rows still on screen, nothing to show (and whether search or filters caused it), the read refused, the last write refused, and a server that answers config-missing at runtime.

Import: @slices.sh/collection/headless

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

```tsx
GateProps<ErrorState>

type ErrorState = { message: string; denied: boolean; retry: () => void }
```

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

### Collection.WriteError

gate

The last write this collection refused.

`Collection.Error` is the *read* refusing — no rows, nothing to show. This is the other
half, and it was missing: a move that the server would not take rolled the card home again
and said nothing, which reads as a board that ignores you. A delete refused from a menu
that has already closed said nothing either.

`denied` means retrying cannot help, so the words differ: "you don't have access to these
records" rather than "try again". `dismiss` closes it; so does the next write that works.

Import: @slices.sh/collection/headless

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

```tsx
GateProps<{ message: string; denied: boolean; dismiss(): void }>
```

Source: @slices.sh/collection/src/headless/write-error.tsx

### Collection.MissingConfiguration

gate

The server this collection reads from answered that it lacks configuration.

The family's own name for the shared gate, with the service already chosen: a screen that
draws a collection should not have to know which string a collection's routes answer with,
and another service's trouble (a transcription key) is not this screen's notice.

Import: @slices.sh/collection/headless

```tsx
GateProps<{ scopes: readonly string[] }>
```

Source: @slices.sh/collection/src/headless/missing-configuration.tsx

### Collection.LoadSampleRows

action · Element: button

Inserts the rows generated from the schema's examples. It renders nothing unless the provider offers development seeding.

Import: @slices.sh/collection/headless

Capability: seed

Collection.LoadSampleRows needs Root.

```tsx
PartProps<"button", { pending: boolean; error: string | null }>
```

Source: @slices.sh/collection/src/headless/load-sample-rows.tsx

### Collection.Spacer

content · Element: div

The empty room a virtualized list keeps where its undrawn rows would be.

Its own part, twice, rather than padding on the viewport, because a `tbody` ignores padding
and a row is the only thing a table will make room for. Nothing at all when the list is
drawn whole, so a short list is the same DOM it always was.

Import: @slices.sh/collection/headless

```tsx
SpacerProps

type SpacerProps = PartProps<"div"> & { side: "before" | "after" }
```

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

### Collection.LoadMore

action · Element: button

A Load more button, a sentinel that loads on scroll, the gate while a page is on its way and the gate at the end.

Import: @slices.sh/collection/headless

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

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

Source: @slices.sh/collection/src/headless/load-more.tsx

### Collection.Sentinel

content · Element: div

The end of a feed, which asks for the next page by coming into view.

It renders only while there is more to read, so a reader who has reached the end is not
left watching a marker that will never do anything. Nothing is drawn into it: what it
carries is the pending state, for a spinner a skin decides on.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"div", { pending: boolean }>
```

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

### Collection.LoadingMore

gate

While a feed is fetching the next page.

Not the first load, which `Collection.Loading` is for and which the skeleton rows already
stand in for: this is the one that appears under records already on screen.

Import: @slices.sh/collection/headless

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

```tsx
GateProps
```

Source: @slices.sh/collection/src/headless/loading-more.tsx

### Collection.Refreshing

gate

While a newer answer to the same query is on its way and the old rows are still on screen.

Not `Collection.Loading`, which is the first load and has skeleton rows to stand in for
what is coming. This is the page turn, the search a keystroke changed, the sort somebody
clicked: the rows are worth keeping and the wait is worth saying, or a table that takes a
second to answer looks like one that did not hear.

Import: @slices.sh/collection/headless

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

```tsx
GateProps
```

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

### Collection.Exhausted

gate

The end of a feed, once there is no more to read.

A feed has no last page to land on, so this is the only thing that ever says the reader
has seen everything. It stays away while the feed is empty: nothing read is not the end.

Import: @slices.sh/collection/headless

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

```tsx
GateProps<{ count: number }>
```

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

### Collection.Owner.Root

scope

The root holds the choice. Select is a native <select> whose value is the chosen principal, "" for none; Any is its <option value=""> for every record. The other options are Principals.PersonalOption and Principals.OrganizationOption.

Import: @slices.sh/collection/headless

```tsx
RootProps & { defaultOwner?: string | null }
```

Alternate import: Owner.Root from @slices.sh/collection/headless/owner

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

### Collection.Owner.Select

input · Element: select

The root holds the choice. Select is a native <select> whose value is the chosen principal, "" for none; Any is its <option value=""> for every record. The other options are Principals.PersonalOption and Principals.OrganizationOption.

Import: @slices.sh/collection/headless

Collection.Owner.Select must be inside <Collection.Owner.Root>.

```tsx
PartProps<"select">
```

Alternate import: Owner.Select from @slices.sh/collection/headless/owner

Source: @slices.sh/collection/src/headless/owner/select.tsx

### Collection.Owner.Any

content · Element: option

The root holds the choice. Select is a native <select> whose value is the chosen principal, "" for none; Any is its <option value=""> for every record. The other options are Principals.PersonalOption and Principals.OrganizationOption.

Import: @slices.sh/collection/headless

```tsx
PartProps<"option">
```

Alternate import: Owner.Any from @slices.sh/collection/headless/owner

Source: @slices.sh/collection/src/headless/owner/any.tsx

### Collection.Owner.Rows

scope

Rows ANDs { owner } into every read below it (lists, counts, facets, live changes) and the writes beside them. Another owner remounts what is inside, so no row, page or selection leaks across.

Import: @slices.sh/collection/headless

Collection.Owner.Rows must be inside <Collection.Owner.Root>.

```tsx
RootProps
```

Alternate import: Owner.Rows from @slices.sh/collection/headless/owner

Source: @slices.sh/collection/src/headless/owner/rows.tsx

### Collection.Owner.WhenAny

gate

WhenAny renders while no owner is chosen; WhenChosen renders once one is, with { owner }.

Import: @slices.sh/collection/headless

Collection.Owner.WhenAny must be inside <Collection.Owner.Root>.

```tsx
GateProps
```

Alternate import: Owner.WhenAny from @slices.sh/collection/headless/owner

Source: @slices.sh/collection/src/headless/owner/when-any.tsx

### Collection.Owner.WhenChosen

gate

WhenAny renders while no owner is chosen; WhenChosen renders once one is, with { owner }.

Import: @slices.sh/collection/headless

Collection.Owner.WhenChosen must be inside <Collection.Owner.Root>.

```tsx
GateProps<{ owner: string }>
```

Alternate import: Owner.WhenChosen from @slices.sh/collection/headless/owner

Source: @slices.sh/collection/src/headless/owner/when-chosen.tsx
