Skip to content
txbrendan/ui

Board

beta

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.

Preview

Tasks by stage. Press a card to open it, or Add to start one.

Wireframes
DesignDue 12 Nov
Help center
WebDue 14 Nov
Push alerts
MobileDue 18 Nov
Invoice templates
PlatformDue 21 Nov
Payments page
WebDue 8 Nov
Onboarding emails
MarketingDue 10 Nov
Team permissions
PlatformDue 6 Nov
Brand refresh
DesignDue 30 Oct
Reports hub
WebDue 28 Oct

Press a card or an Add button

Cards fade in and out, and the count of the lane rolls to match.

Payments page
WebDue 8 Nov
Onboarding emails
MarketingDue 10 Nov
Card issuing
PlatformDue 15 Nov
Team permissions
PlatformDue 6 Nov

Skeleton cards hold each lane while the tasks load.

An empty lane says so and keeps its Add button.

Team permissions
PlatformDue 6 Nov

No items

Press Add item

In a narrow column, the lanes scroll sideways one at a time.

Wireframes
DesignDue 12 Nov
Help center
WebDue 14 Nov
Push alerts
MobileDue 18 Nov
Invoice templates
PlatformDue 21 Nov
Payments page
WebDue 8 Nov
Onboarding emails
MarketingDue 10 Nov
Team permissions
PlatformDue 6 Nov
Brand refresh
DesignDue 30 Oct
Reports hub
WebDue 28 Oct

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>