Board
A kanban board with drag-and-drop between columns — a deals pipeline surface.
A kanban for a deals pipeline: horizontally scrolling columns (a dot + title + live count header, a "New" button, a stack of cards) with native drag-and-drop between columns. The parent owns the cards, so moving one is a controlled state update — drag a card below and drop it in another column.
Its chrome is built for a deals pipeline, but the columns/cards are generic data you supply — use it as-is for any kanban surface.
Installation
npm install @crisp-ui-kit/crispUsage
import { Board, type BoardCard } from "@crisp-ui-kit/crisp";
type Task = BoardCard & { title: string };
const [cards, setCards] = useState<Task[]>([
{ id: "t1", columnId: "todo", title: "Draft the proposal" },
{ id: "t2", columnId: "doing", title: "Design the deck" },
]);
<Board
columns={[
{ id: "todo", title: "To do", accent: "#8a94a6" },
{ id: "doing", title: "In progress", accent: "#266df0" },
{ id: "done", title: "Done", accent: "#22a565" },
]}
cards={cards}
onCardsChange={setCards}
onNewCard={(columnId) => setCards((cs) => [...cs, newTask(columnId)])}
newCardLabel="New task"
renderCard={(card) => <span>{card.title}</span>}
/>;Props
Prop
Type
Keyboard
Native HTML5 drag-and-drop is pointer-only by specification, so the board has its own key model — without it, moving a card would have no keyboard equivalent at all.
| Key | Action |
|---|---|
Tab | Move through the cards; each is focusable and announced as a list item. |
Ctrl/Cmd + ← / → | Move the focused card to the previous or next column. |
Ctrl/Cmd + ↑ / ↓ | With onCardMove, move the focused card up or down within its column. |
The modifier is deliberate. A screen reader owns the bare arrow keys in browse mode, and a board scrolls horizontally, so a plain arrow on a focused card would fight both. At the first or last column the key is left unhandled rather than swallowed, so it still reaches the browser.
Each move is announced through a polite live region, and focus follows the card into its new
column — React remounts it under the new parent, so without that the focus would be lost to
<body> on every move.
BoardColumn is { id: string; title: string; accent?: string } (accent is a CSS colour for the header dot). BoardCard is { id: string; columnId: string } — extend it with your own fields.
Calc footer
Pass calc (#604) for a per-column footer, mirroring the table's own (measured live off a
Deals pipeline): each column gets its own "+ Add calculation" trigger, opening a menu of eight
aggregations — Count empty, Count filled, Percent empty, Percent filled, Sum, Average, Max, Min —
computed over just that column's own cards. getValue reads the number to aggregate off a card
(e.g. a deal's value); return null/undefined for a card with nothing set there. Omit calc
and there's no footer at all, exactly as before.
Notes
- Drag-and-drop. Cards are native
draggableelements; dropping one on a column reparents it throughonCardsChange. The hovered drop column highlights while dragging. - Positioned moves. Pass
onCardMoveand a drop reports{ cardId, fromColumnId, toColumnId, beforeCardId }, so a card can be dropped between two others and reordered inside its column, with a line marking the drop position. The keyboard moves (Ctrl/Cmd with the arrow keys) fire it too. A board withoutonCardMovebehaves as before. - Opening a card.
onCardClickfires on a plain click or Enter on the focused card. It does not fire after a drag, nor for clicks on the card's own buttons and inputs. - Controlled. The board never mutates its own cards — it asks the parent to, so a board and a table can be two views over the same list of records.
- Counts are live. Each column header shows its current card count, derived from
cards. - The calc footer's aggregation set is its own — measured separately from the table's calc
footer (
count/filled/sum/avg/min/maxinRecordsTable); Board's live menu splits "not empty" into a count and a percent, and does the same for "empty". Not reconciled into one shared vocabulary — two different Attio surfaces, two different measured menus.