Board
betaShows 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.
Preview
Tasks by stage. Press a card to open it, or Add to start one.
Press a card or an Add button
Cards fade in and out, and the count of the lane rolls to match.
Skeleton cards hold each lane while the tasks load.
An empty lane says so and keeps its Add button.
Press Add item
In a narrow column, the lanes scroll sideways one at a time.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/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
The root. A row of lanes that scrolls sideways with snap when they do not fit.
BoardProps
- BoardLane
Part. One stage: a soft fill with a title, a count, its cards, and an optional Add button.
BoardLaneProps
- BoardCard
Part. One item, on a card. Its content is yours.
BoardCardProps
- loading
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.
boolean · default false
- lanesLabel
On Board. The name screen readers give the row of lanes.
string · default "Lanes"
- title
Required on BoardLane, where it names the stage, and on BoardCard, where it names the card's button.
string | ReactNode
- count
On BoardLane. How many items the stage holds. It rolls when it changes. Default: the number of cards in the lane.
number
- tone
On BoardLane. The color of the dot beside the title.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- onAdd
On BoardLane. Shows a ghost Add button at the end of the lane, and is called when it is pressed.
() => void
- addLabel
On BoardLane. The Add button's words.
string · default "Add item"
- action
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.
ReactNode
- onOpen
On BoardCard. Makes the whole card one button that opens the item.
() => void
- children
On Board, the lanes. On BoardLane, the cards, each with a key. On BoardCard, the details under the title.
ReactNode
- className
Extra classes on any part. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| 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 | 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 | "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" | "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 | "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
<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
<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
<Board loading={!tasks} aria-label="Tasks by stage">
<BoardLane title="To do">{todo}</BoardLane>
<BoardLane title="In progress" tone="brand">{doing}</BoardLane>
</Board>