crispmeasured from Attio

Board

A kanban board with drag-and-drop between columns — Attio's Deals pipeline surface.

The kanban behind Attio's 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.

Preview

To do2
Draft the proposal
Book the venue
In progress1
Design the deck
Done1
Send invites

Installation

bun add crisp
import "crisp/styles.css";
import { Board } from "crisp";

Usage

import { Board, type BoardCard } from "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>}
/>;

API

Board<T>

PropTypeDefaultDescription
columnsBoardColumn[]The columns, in order. Each is { id, title, accent? }.
cardsT[]The cards (controlled). Each extends { id, columnId }.
onCardsChange(cards: T[]) => voidFires after a drag with the reparented card list.
renderCard(card: T) => ReactNodeRenders a card's body.
onNewCard(columnId: string) => voidWhen set, each column shows a header + and a "New" button.
newCardLabelstring"New"Label for the per-column new-card button.

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.

Notes

  • Drag-and-drop. Cards are native draggable elements; dropping one on a column reparents it through onCardsChange. The hovered drop column highlights while dragging.
  • 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.

On this page