# Board

> Shows items grouped into lanes by stage, side by side, with a count and an Add button on each lane and a card people open for each item.

- Name: `board`
- Status: beta
- Categories: data, layout
- File: the install writes `board.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/button-variants`, `@arbokui/motion-timing`, `@arbokui/rolling-number`, `@arbokui/skeleton`. The install adds them.
- Gallery: https://arbokui.com/components/board
- Registry item: https://arbokui.com/r/board.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/board
   ```

## Use it for

- Work that moves through stages, like tasks, tickets, or requests, when people want to see every stage at once.
- Put one BoardLane per stage inside Board, and one BoardCard per item inside its lane. Give every card a key, so a card that leaves fades out in its own place.
- The lane counts its cards by itself. Pass count only when the lane shows part of a longer list.
- Pass onAdd to show an Add button at the end of a lane. Name it with addLabel when the items have a name, like "Add task".
- Pass onOpen to make a card one button. Badges, links, and buttons inside the card still take their own presses.
- The lanes share the width when they fit. When they do not, the row scrolls sideways lane by lane, and each edge with lanes past it fades out.
- Set loading while the items load. Each lane shows skeleton cards, and the cards fade in over them in a wave when it turns false. A board that already has cards waits 150 ms first, so a quick load never flashes skeletons.
- Board shows what it is given and does not fetch, so it has no error state. When the items fail to load, show a Notice or Alert above the board with a way to try again, and keep the lanes empty.

## Avoid it for

- A long list people scan or sort. Use Table or List Row.
- A single stage. A list says the same thing with less.
- Dragging cards between lanes. Cards do not drag. Offer a Move action in the card instead.

## Props

- `Board`: `BoardProps`. The root. A row of lanes that scrolls sideways with snap when they do not fit.
- `BoardLane`: `BoardLaneProps`. Part. One stage: a soft fill with a title, a count, its cards, and an optional Add button.
- `BoardCard`: `BoardCardProps`. Part. One item, on a card. Its content is yours.
- `loading`: `boolean`. Default `false`. On Board. Each lane shows skeleton cards that hold the space, and the counts wait. A board with cards waits 150 ms before it swaps.
- `lanesLabel`: `string`. Default `"Lanes"`. On Board. The name screen readers give the row of lanes.
- `title`: `string | ReactNode`. Required on BoardLane, where it names the stage, and on BoardCard, where it names the card's button.
- `count`: `number`. On BoardLane. How many items the stage holds. It rolls when it changes. Default: the number of cards in the lane.
- `tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. On BoardLane. The color of the dot beside the title.
- `onAdd`: `() => void`. On BoardLane. Shows a ghost Add button at the end of the lane, and is called when it is pressed.
- `addLabel`: `string`. Default `"Add item"`. On BoardLane. The Add button's words.
- `action`: `ReactNode`. On BoardLane. A control at the end of the lane's header, like an icon button that adds an item at the top of the lane.
- `onOpen`: `() => void`. On BoardCard. Makes the whole card one button that opens the item.
- `children`: `ReactNode`. On Board, the lanes. On BoardLane, the cards, each with a key. On BoardCard, the details under the title.
- `className`: `string`. Extra classes on any part. Later classes win.

## Accessibility

- Each lane is a group named by its title and count, like "To do, 4 items". While the board loads, it says "To do, loading".
- A card with onOpen holds one real button, stretched over the card and named by the card's title. Tab reaches it, and Enter or Space opens it.
- Buttons and links inside a card sit above that button, so each is its own Tab stop and takes its own press.
- Tab moves through the cards lane by lane. When focus lands in a lane that is cut off or under an edge fade, the row scrolls that lane fully into view.
- When the row scrolls and nothing in it takes focus, the row itself is one Tab stop, so the arrow keys can scroll it. It is a group named by lanesLabel, "Lanes" by default, and its focus ring sits inside its edge.
- A card takes no press until it has faded halfway in, so a card that cannot be seen yet cannot be opened.
- A card that leaves is hidden from screen readers and from Tab while it fades out.
- The skeleton cards are hidden from screen readers, and the board has aria-busy while it loads.
- Forced-colors mode draws a border around each lane, since it paints the lane's fill away, and turns off the edge fades.
- The cards fade up in a wave. Reduced motion takes out the rise, and motion off shows everything at once.

## Examples

### Tasks by stage

```tsx
<Board aria-label="Tasks by stage">
  {stages.map((stage) => (
    <BoardLane key={stage.id} title={stage.title} tone={stage.tone} onAdd={() => addTask(stage.id)}>
      {stage.tasks.map((task) => (
        <BoardCard key={task.id} title={task.title} onOpen={() => openTask(task.id)}>
          <span className="text-xs text-muted-foreground">Due {task.due}</span>
        </BoardCard>
      ))}
    </BoardLane>
  ))}
</Board>
```

### A lane that shows part of a longer list

```tsx
<BoardLane title="Done" tone="success" count={128} addLabel="Add task" onAdd={addTask}>
  {recent.map((task) => (
    <BoardCard key={task.id} title={task.title} />
  ))}
</BoardLane>
```

### While the items load

```tsx
<Board loading={!tasks} aria-label="Tasks by stage">
  <BoardLane title="To do">{todo}</BoardLane>
  <BoardLane title="In progress" tone="brand">{doing}</BoardLane>
</Board>
```
