TaskBoard

Horizontal status columns for the Tasks board view. Pair with TaskDnd for drag targets and TaskCard for items.

Preview#

import {
  TaskBoardColumn,
  TaskBoardColumnBadge,
  TaskBoardColumnChevron,
  TaskBoardColumnCount,
  TaskBoardColumnEmpty,
  TaskBoardColumnEmptyAllowed,
  TaskBoardColumnEmptyBlocked,
  TaskBoardColumnEmptyIdle,
  TaskBoardColumnHeading,
  TaskBoardColumnItems,
  TaskBoardColumnPanel,
  TaskBoardColumnTrigger,
  TaskBoardRoot,
  TaskCard,
  TaskDndProvider,
  TaskDragOverlay,
  TaskDraggable,
  TaskDropPlaceholder,
  TaskDropZone,
} from "raft-ui";

<TaskDndProvider isDropAllowed={() => true} onMove={() => {}}>
  <TaskBoardRoot>
    <TaskBoardColumn
      render={<TaskDropZone id="task-status:todo" data={{ status: "todo" }} />}
    >
      <TaskBoardColumnTrigger>
        <TaskBoardColumnHeading>
          <TaskBoardColumnBadge status="todo" />
          <TaskBoardColumnCount />
        </TaskBoardColumnHeading>
        <TaskBoardColumnChevron />
      </TaskBoardColumnTrigger>
      <TaskBoardColumnPanel>
        <TaskBoardColumnItems>
          <TaskBoardColumnEmpty>
            <TaskBoardColumnEmptyIdle />
            <TaskBoardColumnEmptyAllowed />
            <TaskBoardColumnEmptyBlocked />
          </TaskBoardColumnEmpty>
          <TaskDropPlaceholder id="task-1" placement="before" />
          <TaskDraggable
            id="task-1"
            dropId="task-1"
            dropData={{ status: "todo", taskId: "task-1" }}
          >
            <TaskCard />
          </TaskDraggable>
          <TaskDropPlaceholder id="task-1" placement="after" />
        </TaskBoardColumnItems>
      </TaskBoardColumnPanel>
    </TaskBoardColumn>
  </TaskBoardRoot>
  <TaskDragOverlay>
    <TaskCard />
  </TaskDragOverlay>
</TaskDndProvider>

Wrap columns in TaskDndProvider and mount each column as a TaskDropZone. Drop policy and move handlers stay in the composition.

Scroll area#

Keep one ScrollAreaViewport around the whole board. The viewport owns vertical and horizontal scrolling; TaskBoardRoot keeps its natural minimum width, so columns never become independent scroll regions.

Sparse tasks leave In Review / Done / Closed empty so TaskBoardColumnEmptyIdle shows. Drag onto an empty column for Allowed / Blocked copy (Closed is not blocked in this demo — see TaskDnd).

Keep one shared ScrollAreaViewport, and mount a useVirtualizer stack inside each tall column. Only viewport rows plus overscan stay mounted — watch the counter while scrolling. Virtualization stays consumer-owned; TaskBoard does not provide it.

Mount a virtual stack when a column can grow past the shared viewport. Only visible rows plus overscan stay in the DOM.

Mounted 0 / 48

API#WIP

...props
useRender.ComponentProps<"div">
Horizontal board layout root.
open
boolean
Controlled collapsed state for the column.
defaultOpendefault: false
boolean
Initial collapsed state for the column.
...props
Collapsible.Root.Props
Fixed-width board column root. Compose TaskDropZone via render for DnD.
...props
Collapsible.Trigger.Props
Expands or collapses the board column.
...props
useRender.ComponentProps<"span">
Heading row for column badge and count.
status
"todo" | "in-progress" | "in-review" | "done" | "closed"
Task status that drives the badge surface color.
...props
useRender.ComponentProps<"span">
Column status badge.
...props
useRender.ComponentProps<"span">
Task count in the column heading.
...props
useRender.ComponentProps<"span">
Disclosure chevron for the column trigger.
...props
Collapsible.Panel.Props
Collapsible panel hosting column items and empty states.
...props
useRender.ComponentProps<"div">
Item list inside a board column.
...props
useRender.ComponentProps<"div">
Empty-column host for idle, allowed, and blocked drop copy.
...props
useRender.ComponentProps<"span">
Idle empty-column message.
...props
useRender.ComponentProps<"span">
Allowed drop-target empty-column message.
...props
useRender.ComponentProps<"span">
Blocked drop-target empty-column message.