Board
A kanban board on UI SDK createTransfer: one tab stop, arrows across cards and columns, Space to carry a card, drag with the pointer, column limits, add in place, a details dialog to edit, move or delete, filters, and agent actions.
A kanban board on UI SDK's createTransfer: cards in columns that people move with the
pointer, the keyboard or an agent, with the same rules and announcements for all three.
pnpm dlx shadcn@latest add superzero11/uiset/boardUse
const [board] = useState(createBoardController)
<BoardToolbar label="Sprint board" query={query} onQueryChange={setQuery}
people={people} person={person} onPersonChange={setPerson} />
<Board
label="Sprint board"
controller={board}
columns={[
{ id: "todo", title: "To do" },
{ id: "doing", title: "In progress", limit: 3 },
{ id: "done", title: "Done" },
]}
cards={cards}
lanes={lanes}
onLanesChange={(next, { key, from, to, index }) => setLanes(next)}
onCardOpen={setOpen}
onAddCard={(column, title) => add(column, title) /* return the new card's id */}
filter={(card) => matches(card, query, person)}
/>
<BoardCardDialog board={board} card={open} column={columnOf(open)} columns={columns}
people={people} labels={labels}
onSave={save} onDelete={remove} onClose={() => setOpen(null)} />
lanesmaps each column to its card ids, in order.cardsholds the cards by id.- A column's
limitrefuses cards past it, for people (the move skips to the next column with room), for drags, and for agents (with a reason). filterhides cards that don't match without moving them; focus skips them, and the board's one tab stop never stays on a hidden card. While a filter hides cards, nothing moves: a carried card would step through cards nobody can see. Space says so.onAddCardreturns the new card's id. A controlled board's owner adds it tolanes; an uncontrolled board adds it itself. When it fills the column, focus goes to the new card.controller(fromcreateBoardController()) connectsBoardCardDialog, or your code, to the board:moveCard,addCard,hasRoom,focusCard,cardElement,neighbor. The dialog moves cards through it, so a move keeps column limits, reachesonLanesChange(and your Undo toast), and focus goes back to the card, or to its neighbor after a delete or an edit the filter hides.BoardCardDialogedits the assignee only when givenpeople, and labels only when givenlabels; labels it doesn't list are kept. Full columns are listed but can't be chosen.renderCardreplaces the card face;BoardCardFaceis the default.describeCardgives screen readers the details after the title (priority, due date, person and labels by default).lockedmarks cards that can't move.messagesreplaces the help, empty, full and filtered copy;moveMessagereplaces the move announcements (for other languages).todaymarks overdue cards; pass the real date.
Touch
A finger scrolls the board from anywhere on a card, and drags it from the grip at its top corner, which shows on touch screens only. The keyboard and agents don't need it.
Known limits
Releasing a dragged card over a full column, or outside the board, drops it in the last
column it passed that had room. That's UI SDK's createTransfer (reported upstream).
Keys
| Keys | Does |
|---|---|
| Tab | Into the board (one stop) and out |
| Up, Down, Home, End | Cards in a column |
| Left, Right | The card at the same height in the next column with cards |
| Space | Pick a card up; arrows carry it; Space drops it |
| Escape | Put a carried card back |
| Enter | Open the card's details: edit, move to a column, delete |
Every pickup, move and drop is announced ("Write the spec, moved to Doing, position 2 of
3"). Cards are options in a listbox per column, with aria-roledescription "card" and
"column", the way UI SDK's sortable lists describe themselves; the carried card is the
selected one.
For agents
moveCard(card, column, position?), addCard(column, title) and openCard(card), by the
titles people see. The snapshot's items list every card with its column, assignee and due
date. A board without onCardOpen or onAddCard tells agents it can't open or add cards.
Deleting goes through the dialog's confirm, marked data-agent-confirm.