# Kanban board

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

Import: @slices.sh/collection/headless

Public parts: 14

## 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.Board
    Collection.BoardTabs
      Collection.LaneTab
    Collection.BoardPanel
      Collection.Lane
        Collection.LaneLoading
        Collection.LaneError
        Collection.LaneCount
        Collection.MoveTo
        Collection.MoveWithin
        Collection.LaneLabel
    Collection.DragStatus
    Collection.Ungrouped
  CollectionPointer.Root
    CollectionPointer.Lane
```

## 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.Board>
        <Collection.Lane>
          <Collection.LaneLabel />
          <Collection.LaneCount />
          <Collection.Row render={<div />}>
            <Collection.Cell render={<span />}>
              <Collection.Value />
            </Collection.Cell>
          </Collection.Row>
        </Collection.Lane>
      </Collection.Board>
    </Collection.Root>
  )
}

```

Choose a grouping field in the collection configuration so the board has lanes.

## Complete API reference

### Collection.Board

scope

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

Board requires Collection.Root.

```tsx
BoardProps

type BoardProps = RootProps & { pageSize?: number }
```

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

### Collection.Lane

content · Element: section

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

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

```tsx
LaneProps & { limit?: number | Readonly<Record<string, number>> }

type LaneProps = PartProps<
  "section",
  {
    option: string
    count: number | null
    over: boolean
    dropIndex: number | null
  }
>
```

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

### CollectionPointer.Lane

content · Element: section

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.Lane requires Collection.Lane.

```tsx
PartProps<"section">
```

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

### Collection.LaneLabel

content

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

Collection.LaneLabel must be inside <Collection.Lane>.

```tsx
PartProps<null>
```

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

### Collection.LaneCount

content · Element: span

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

Collection.LaneCount must be inside <Collection.Lane>.

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

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

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

### Collection.BoardTabs

content · Element: div

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

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

Source: @slices.sh/collection/src/headless/board-tabs.tsx

### Collection.LaneTab

action · Element: button

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

LaneTab requires Collection.Root.

LaneTab requires Collection.Board.

```tsx
LaneTabProps

type LaneTabProps = PartProps<"button", { label: string; count: number | null; active: boolean }>
```

Source: @slices.sh/collection/src/headless/lane-tab.tsx

### Collection.BoardPanel

content · Element: div

Inactive panels keep their ARIA targets, but only one grid is mounted on mobile.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

BoardPanel requires Collection.Root.

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

Source: @slices.sh/collection/src/headless/board-panel.tsx

### Collection.MoveTo

action · Element: button

Explicit moves a finger and a screen reader can make: to another lane, or to the top or bottom of this one, and the live announcement of a drag. MoveWithin renders nothing without a numeric orderBy.

Import: @slices.sh/collection/headless

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

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

```tsx
MoveToProps

type MoveToProps = PartProps<"button", { lane: string; label: string }>
```

Source: @slices.sh/collection/src/headless/move-to.tsx

### Collection.MoveWithin

action · Element: button

Sends a card to the top or the bottom of the lane it is already in.

A pointer drag reorders a card and so does the keyboard carry; a finger does neither, because
HTML5 drag events never fire from touch. So the two ends of a lane are offered as ordinary
buttons — which is the same answer Atlassian's own accessibility guidance gives for
pragmatic-drag-and-drop: an explicit move a reader can name, not a gesture.

Away when the schema declares no numeric `orderBy`, because then a lane holds no order to
move within, and away when the card is already at that end.

Import: @slices.sh/collection/headless

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

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

```tsx
MoveWithinProps

type MoveWithinProps = PartProps<"button", { to: Edge }> & { to: Edge }

type Edge = "top" | "bottom"
```

Source: @slices.sh/collection/src/headless/move-within.tsx

### Collection.DragStatus

content · Element: span

Explicit moves a finger and a screen reader can make: to another lane, or to the top or bottom of this one, and the live announcement of a drag. MoveWithin renders nothing without a numeric orderBy.

Import: @slices.sh/collection/headless

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

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

type DragStatusState = {
  carried: boolean
  status: "idle" | "moving" | "dropped" | "canceled"
  lane: string | null
  index: number | null
}
```

Source: @slices.sh/collection/src/headless/drag-status.tsx

### Collection.LaneLoading

gate

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

LaneLoading requires Collection.Lane.

LaneLoading requires Collection.Root.

```tsx
GateProps
```

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

### Collection.LaneError

gate

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

LaneError requires Collection.Lane.

LaneError requires Collection.Root.

```tsx
GateProps<LaneErrorState>

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

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

### Collection.Ungrouped

gate

A board that reads lane by lane, tabs and one panel per lane on a phone, a lane per option of the group field with its label, backend count, loading and error, and the gate that says there is nothing to group by. LaneCount hides an unknown count rather than counting loaded cards.

Import: @slices.sh/collection/headless

Capability: countMatching (LaneCount)

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

```tsx
GateProps
```

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

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