# Select and edit records

Build selection & actions on your collection's schema, query and permissions.

Import: @slices.sh/collection/headless

Public parts: 32

## 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.View name="table"
    Collection.Grid
      Collection.Row
        Collection.RowMenuTrigger
        Collection.SelectRow
        Collection.DeleteRow
  Collection.RowMenu
    Collection.RowMenuItem
  Collection.SelectAll
  Collection.WhenSelected
    Collection.ClearSelection
    Collection.BulkBar
      Collection.SelectionCount
      Collection.BulkAction
    Collection.SelectMatching
    Collection.DeleteSelected
  Collection.WhenPalette
    Collection.Palette
      Collection.PaletteSearch
      Collection.PaletteItems
      Collection.PaletteEmpty
      Collection.PaletteClose
      Collection.PaletteCount
      Collection.PaletteError
  Collection.PaletteTrigger
  Collection.Shortcuts
  Collection.ShortcutsTrigger
  Collection.BulkDelete.Root
    Collection.BulkDelete.Trigger
    Collection.BulkDelete.Popup
      Collection.BulkDelete.Title
      Collection.BulkDelete.Description
      Collection.BulkDelete.Cancel
      Collection.BulkDelete.Confirm
      Collection.BulkDelete.Error
```

## 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.SelectAll>Select all</Collection.SelectAll>
      <Collection.Row render={<div />}>
        <Collection.SelectRow />
        <Collection.Cell render={<span />}>
          <Collection.Value />
        </Collection.Cell>
      </Collection.Row>
      <Collection.WhenSelected>
        <Collection.SelectionCount />
        <Collection.ClearSelection>
          Clear selection
        </Collection.ClearSelection>
      </Collection.WhenSelected>
    </Collection.Root>
  )
}

```

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

## Command palette

```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.PaletteTrigger>Search commands</Collection.PaletteTrigger>
      <Collection.Palette
        label="Bulk actions"
        commandLabel={({ field, value }) =>
          "Set " + field.label + " to " + value
        }
      >
        <Collection.PaletteSearch />
        <Collection.PaletteItems />
        <Collection.PaletteEmpty>No results.</Collection.PaletteEmpty>
        <Collection.PaletteClose>Close</Collection.PaletteClose>
      </Collection.Palette>
    </Collection.Root>
  )
}

```

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

## Complete API reference

### Collection.SelectRow

input · Element: input

Ticking a row or the rows on screen, the gate around what shows while something is selected, the count (null when everything matching is selected and the provider did not count) and letting go.

Import: @slices.sh/collection/headless

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

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

```tsx
PartProps<"input", { checked: boolean }>
```

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

### Collection.SelectAll

input · Element: input

Ticking a row or the rows on screen, the gate around what shows while something is selected, the count (null when everything matching is selected and the provider did not count) and letting go.

Import: @slices.sh/collection/headless

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

```tsx
PartProps<"input", SelectAllState>

type SelectAllState = { checked: boolean; indeterminate: boolean }
```

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

### Collection.WhenSelected

gate

While anything is selected, and how much.

`matching` says the selection is the filter rather than a list of ids, and `count` is then
the whole result — which is the number a bulk action is about to touch.

Import: @slices.sh/collection/headless

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

```tsx
GateProps<{ count: number; matching: boolean }>
```

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

### Collection.BulkBar

content · Element: div

A toolbar that appears while records are selected, its buttons, turning the selection into the filter, and a delete that arms on the first press. SelectMatching renders nothing when the provider did not count or the page is already the whole result.

Import: @slices.sh/collection/headless

Capability: countMatching (SelectMatching)

Collection.BulkBar requires Collection.Root.

```tsx
PartProps<"div", { count: number; matching: boolean }> & {
  /** The animated toolbar the bar renders into; its `frame` places it (a bar floating over a list). */
  toolbar?: ReactElement<ToolbarProps>
}

type ToolbarProps = ComponentProps<typeof MotionToolbar>
```

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

### Collection.SelectionCount

content · Element: span

Ticking a row or the rows on screen, the gate around what shows while something is selected, the count (null when everything matching is selected and the provider did not count) and letting go.

Import: @slices.sh/collection/headless

Collection.PaletteCount requires Collection.Root.

```tsx
PartProps<"span", { count: number | null; matching: boolean }>
```

Source: @slices.sh/collection/src/headless/palette-count.tsx

### Collection.SelectMatching

action · Element: button

"Select all 312" — the selection stops being a list of ids and becomes the filter itself.

Without it, bulk work over a filter is unreachable: ticking two hundred of three hundred
rows is two hundred clicks, and `removeMany(filter)` and `updateMany(filter, patch)` sit in
the contract with no way to ask for them. With it, "delete selected" is one statement.

Away when the backend did not count the rows — "all of them" with no number is not an offer
anybody can judge — and away once the whole page is already the whole result, or when the
selection is already the filter.
→ docs/rules/capability-honesty.md

Import: @slices.sh/collection/headless

Capability: countMatching (SelectMatching)

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

```tsx
PartProps<"button", { count: number }>
```

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

### Collection.BulkAction

action · Element: button

A toolbar that appears while records are selected, its buttons, turning the selection into the filter, and a delete that arms on the first press. SelectMatching renders nothing when the provider did not count or the page is already the whole result.

Import: @slices.sh/collection/headless

Capability: countMatching (SelectMatching)

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

Source: @slices.sh/collection/src/headless/bulk-action.tsx

### Collection.DeleteSelected

action · Element: button

Deletes what is ticked — and asks first.

Two presses rather than a dialog: one click and gone was the whole of the safety here, and a
modal is the wrong shape on a phone. `armed` is the question, so a skin restyles it and a
copy dictionary asks it in its own words; the question goes away when focus leaves the
button or the selection changes under it, because a button left asking is a trap.

What it is asking *about* is the selection itself, rather than a flag plus an effect to
clear it: a question about these three rows is not an answer about those three, so the
moment the selection moves the button is no longer armed, with no render spent noticing.

In `matching` mode this is one statement over the filter rather than a list of ids, so
"delete all 312" never has to read page two.

Import: @slices.sh/collection/headless

Capability: countMatching (SelectMatching)

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

```tsx
PartProps<"button", { pending: boolean; count: number; armed: boolean }>
```

Source: @slices.sh/collection/src/headless/delete-selected.tsx

### Collection.ClearSelection

action · Element: button

Ticking a row or the rows on screen, the gate around what shows while something is selected, the count (null when everything matching is selected and the provider did not count) and letting go.

Import: @slices.sh/collection/headless

Collection.ClearSelection requires Collection.Root.

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

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

### Collection.RowMenu

content · Element: div

One popup per collection, anchored by the selected row's native detached trigger.

Import: @slices.sh/collection/headless

Collection.RowMenu requires Collection.Root.

```tsx
PartProps<"div"> & {
  /** The menu popup and its positioner, which the pattern styles. */
  popup?: ReactElement<ComponentProps<typeof MotionMenu>>
}
```

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

### Collection.RowMenuTrigger

action · Element: button

One menu per root, rendered outside the rows, opened from a trigger inside a row and scoped to that row; its items, and deleting the row.

Import: @slices.sh/collection/headless

Collection.RowMenuTrigger requires Collection.Row.

Collection.RowMenuTrigger requires Collection.Root.

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

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

### Collection.RowMenuItem

action · Element: button

One menu per root, rendered outside the rows, opened from a trigger inside a row and scoped to that row; its items, and deleting the row.

Import: @slices.sh/collection/headless

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

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

### Collection.DeleteRow

action · Element: button

One menu per root, rendered outside the rows, opened from a trigger inside a row and scoped to that row; its items, and deleting the row.

Import: @slices.sh/collection/headless

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

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

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

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

### Collection.PaletteTrigger

action · Element: button

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.PaletteTrigger requires Collection.Root.

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

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

### Collection.Palette

scope

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.Palette requires Collection.Root.

```tsx
RootProps & {
  label: string
  commandLabel: BulkCommandLabel
  /** The dialog the commands open in, which the pattern styles. */
  popup?: ReactElement<ComponentProps<typeof MotionDialog>>
}
```

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

### Collection.WhenPalette

gate

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.WhenPalette requires Collection.Root.

```tsx
GateProps
```

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

### Collection.PaletteCount

content · Element: span

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.PaletteCount requires Collection.Root.

```tsx
PartProps<"span", { count: number | null; matching: boolean }>
```

Source: @slices.sh/collection/src/headless/palette-count.tsx

### Collection.PaletteClose

action · Element: button

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

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

Source: @slices.sh/collection/src/headless/palette-close.tsx

### Collection.PaletteSearch

input · Element: input

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.PaletteSearch requires Collection.Palette.

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

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

### Collection.PaletteItems

content · Element: div

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.PaletteItems requires Collection.Palette.

Collection.PaletteItems requires Collection.Root.

```tsx
Props

type Props = PartProps<"div", { label: string; pending: boolean }>
```

Source: @slices.sh/collection/src/headless/palette-items.tsx

### Collection.PaletteEmpty

content · Element: div

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

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

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

### Collection.PaletteError

content · Element: p

A searchable dialog of field and value commands over the selection, written with one bulk update. WhenPalette renders nothing when the root has no palette or the provider cannot update many records at once.

Import: @slices.sh/collection/headless

Capability: bulkUpdate

Collection.PaletteError requires Collection.Palette.

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

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

### Collection.ShortcutsTrigger

action · Element: button

A modal sheet of the shortcuts this root answers to, also opened with ?, and its visible trigger.

Import: @slices.sh/collection/headless

Collection.ShortcutsTrigger requires Collection.Root.

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

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

### Collection.Shortcuts

scope

A modal sheet of the shortcuts this root answers to, also opened with ?, and its visible trigger.

Import: @slices.sh/collection/headless

Collection.Shortcuts requires Collection.Root.

```tsx
RootProps &
  ShortcutFeatures & {
    title: string
    labels?: Partial<Record<RowCommand, string>>
    /** The dialog the shortcuts open in, which the pattern styles. */
    popup?: ComponentProps<typeof KeymapSheet>["popup"]
  }
```

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

### Collection.BulkDelete.Root

scope

The root, the trigger that opens a native alert dialog, and the popup.

Import: @slices.sh/collection/headless

BulkDelete.Root requires Collection.Root.

```tsx
RootProps
```

Alternate import: BulkDelete.Root from @slices.sh/collection/headless/bulk-delete

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

### Collection.BulkDelete.Trigger

action · Element: button

The root, the trigger that opens a native alert dialog, and the popup.

Import: @slices.sh/collection/headless

BulkDelete.Trigger requires Collection.Root.

BulkDelete.Trigger requires BulkDelete.Root.

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

Alternate import: BulkDelete.Trigger from @slices.sh/collection/headless/bulk-delete

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

### Collection.BulkDelete.Popup

content · Element: div

The root, the trigger that opens a native alert dialog, and the popup.

Import: @slices.sh/collection/headless

BulkDelete.Popup requires BulkDelete.Root.

BulkDelete.Popup requires Collection.Root.

```tsx
PartProps<"div"> & {
  /** The alert dialog, its backdrop and the viewport that places it, which the pattern styles. */
  popup?: ReactElement<ComponentProps<typeof MotionAlertDialog>>
}
```

Alternate import: BulkDelete.Popup from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/popup.tsx

### Collection.BulkDelete.Title

content · Element: h2

Title and description, given { count, matching } so they can name the scope. count is null when everything matching is selected and the provider did not count.

Import: @slices.sh/collection/headless

BulkDelete.Title requires BulkDelete.Root.

```tsx
PartProps<"h2", { count: number | null; matching: boolean }>
```

Alternate import: BulkDelete.Title from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/title.tsx

### Collection.BulkDelete.Description

content · Element: p

Title and description, given { count, matching } so they can name the scope. count is null when everything matching is selected and the provider did not count.

Import: @slices.sh/collection/headless

BulkDelete.Description requires BulkDelete.Root.

```tsx
PartProps<"p", { count: number | null; matching: boolean }>
```

Alternate import: BulkDelete.Description from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/description.tsx

### Collection.BulkDelete.Cancel

action · Element: button

Cancel (the first focus), Confirm with { count, pending }, and the refusal, which stays in the dialog.

Import: @slices.sh/collection/headless

Capability: bulkDelete

BulkDelete.Cancel requires BulkDelete.Root.

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

Alternate import: BulkDelete.Cancel from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/cancel.tsx

### Collection.BulkDelete.Confirm

action · Element: button

Cancel (the first focus), Confirm with { count, pending }, and the refusal, which stays in the dialog.

Import: @slices.sh/collection/headless

Capability: bulkDelete

BulkDelete.Confirm requires BulkDelete.Root.

BulkDelete.Confirm requires Collection.Root.

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

Alternate import: BulkDelete.Confirm from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/confirm.tsx

### Collection.BulkDelete.Error

content · Element: p

Cancel (the first focus), Confirm with { count, pending }, and the refusal, which stays in the dialog.

Import: @slices.sh/collection/headless

Capability: bulkDelete

BulkDelete.Error requires BulkDelete.Root.

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

Alternate import: BulkDelete.Error from @slices.sh/collection/headless/bulk-delete

Source: @slices.sh/collection/src/headless/bulk-delete/error.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

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