Skip to content

Components · Workflow

KanbanBoard

Columns of cards moved by drag (mouse, pen, touch) or keyboard, with counts, WIP limits, collapse and optimistic moves.

@rookcom/monarch-react/collaborationComponent

Board with a WIP limit

Backlog

3 cards
  • Draft release notesDocs
  • Audit colour contrastDesign
  • Plan onboarding surveyResearch

In progress

3 cards, limit 2, over the limit
  • Fix export timeoutBug
  • Billing settings pageFeature
  • Update help articlesDocs

Review

1 card
  • Keyboard shortcutsFeature

Done

0 cards
    No cards
    Press Space or Enter to pick up. Arrow up and down move within a column, arrow left and right move between columns. Space or Enter drops, Escape cancels.

    Usage examples

    Allowed moves, Move to menu and optimistic saving

    Collapsible columns, enforced limits, drag from the card

    Purpose

    Shows work as cards in columns, such as stages or statuses, and lets people move cards between and within columns by dragging or from the keyboard. The product renders the cards and saves each move.

    Use when

    • Items move through a small set of stages: a task board, a hiring pipeline, a deal pipeline, filings in review.
    • The position of a card within its column carries meaning (priority or rank) and people reorder it.
    • A stage has a work-in-progress limit people need to see.

    Avoid when

    • The order of a single list is all that changes: use ReorderableList.
    • There are many columns of attributes to compare across rows: use DataTable.
    • Stages are a fixed sequence the user walks through once: use Stepper.

    Design principles

    • Cards are a slot: the product draws them with renderCard; the board adds the grip and, optionally, the "Move to…" menu around them.
    • Every drag has a keyboard equivalent and a single-pointer alternative (moveMenu, WCAG 2.5.7). Nothing is drag-only.
    • Moves are announced at every step in a live region, in the same words ReorderableList uses.
    • Persistence belongs to the product: onMove gets the move and the next columns; returning a promise makes the move optimistic with rollback.
    • Rules are injected, never built in: canMove and enforceWipLimits decide which columns accept a card, and refused columns say so while dragging.
    • Colour is never the only signal: an over-limit count also shows a warning icon and "count / limit", and refused columns change border style and show text.

    API reference

    import { KanbanBoard } from '@rookcom/monarch-react/collaboration';
    ExportKindRenderingRoot class
    KanbanBoardcomponentclient.mn-kanban-board

    KanbanBoard is generic in the card type: <KanbanBoard<Task> … />. getItemId, getItemLabel and renderCard are required.

    columns (controlled) or defaultColumns (uncontrolled): { id, title, items, wipLimit?, icon?, actions?, footer?, empty? }.

    onMove(move, nextColumns): move is { item, itemId, fromColumnId, fromIndex, toColumnId, toIndex }. Return a promise for an optimistic move; a rejection puts the card back, announces the failure and calls onMoveError.

    A controlled board keeps a resolved move on screen until the next columns prop arrives, so a refetch can land without the card flickering back.

    The ref is the root <div>, which becomes a region landmark when given aria-label or aria-labelledby. Other div attributes pass through.

    The drag helpers (kanban-board.drag.ts) are internal; they are written to be shared with ReorderableList once both have landed.

    Generated from the TypeScript declarations of packages/react/src/components/workflow/kanban-board.

    Properties

    KanbanBoard

    A board of columns whose cards can be dragged between and within columns with a mouse, pen or touch, or moved from the keyboard: Space picks a card up, the arrow keys move it, Space drops it and Escape cancels. Every step is announced. Columns show counts and WIP limits and can collapse. The product renders the cards and persists moves through `onMove` (optimistic, with rollback when its promise rejects).

    PropTypeDescription
    getItemId required(item: T) => stringA stable, unique key for each card.
    getItemLabel required(item: T) => stringPlain-text name of each card, used in labels and announcements.
    renderCard required(item: T, state: KanbanCardState) => ReactNodeDraws a card's content. The move handle (and menu) are added around it.
    canMove((item: T, toColumnId: string, fromColumnId: string) => boolean)Whether a card may move to a column (its own column is always allowed).
    classNamestring
    collapsedColumnIdsreadonly string[]Collapsed column ids (controlled).
    collapsiblebooleanShow a collapse button in every column header.
    columnsreadonly KanbanColumn<T>[]The columns and their cards (controlled). Update them from `onMove`.
    defaultCollapsedColumnIdsreadonly string[]Collapsed column ids at first render (uncontrolled).
    defaultColumnsreadonly KanbanColumn<T>[]The starting columns when uncontrolled.
    disabledbooleanNo card can be moved.
    dragFrom"card" | "handle"`handle` (default): drag from the grip. `card`: a mouse or pen can also drag from anywhere on the card except its buttons, links and fields; touch still uses the grip so the column can scroll.
    enforceWipLimitsbooleanA column at or over its `wipLimit` refuses new cards.
    headingLevel3 | 4 | 6 | 2 | 5Heading level of the column titles. Default 3.
    idstring
    isItemDisabled((item: T) => boolean)A card that cannot be moved, such as one the user may not edit.
    messagesPartial<KanbanBoardMessages>Override labels and announcements.
    moveMenubooleanAdd a "Move to…" menu to every card: a single-pointer alternative to dragging (WCAG 2.5.7). The card goes to the end of the chosen column.
    onCollapsedColumnIdsChange((ids: string[]) => void)
    onMove((move: KanbanMove<T>, columns: KanbanColumn<T>[]) => unknown)Called once per finished move with the move and the columns after it. The product persists the move here. Return a promise to keep the card in its new place while it is pending: if it rejects, the card goes back and the failure is announced. Controlled boards should pass the new `columns` before the promise resolves (or right after, from a refetch).
    onMoveError((error: unknown, move: KanbanMove<T>) => void)Called when the promise returned by `onMove` rejects.
    refForwardedRef<HTMLDivElement>

    Variants

    PropValuesDescription
    dragFrom'handle''card'handle (default): drag from the grip. card: a mouse or pen can drag from anywhere on the card except its buttons, links and fields; touch keeps using the grip so the column still scrolls.
    moveMenu'true''false'Adds a "Move to…" menu button to each card listing the other columns; refused columns are disabled. The card goes to the end of the chosen column.
    collapsible'true''false'Adds a collapse toggle to every column header. A collapsed column is a narrow strip with its title and count, and still accepts dropped cards.
    enforceWipLimits'true''false'Columns at or over their wipLimit refuse new cards. Without it, limits are shown but not enforced.

    States

    StateExposed asBehaviour
    Dragging (pointer)data-dragging="pointer" on the root and the cardA copy of the card follows the pointer; the card’s slot shows as a dashed placeholder where it would land.
    Dragging (keyboard)data-dragging="keyboard", aria-pressed="true" on the gripThe picked-up card is raised with an accent border and moves as keys are pressed.
    Drop targetdata-drop="allowed" | "blocked", data-overWhile dragging, the column under the card is highlighted; columns that refuse the card get a dashed border and "Not allowed here".
    WIP limitdata-wip="at" | "over"The count turns warning at the limit and danger over it, with a warning icon, and the text alternative says so.
    Pendingdata-pending, aria-busyThe card’s move is waiting for the onMove promise; it is shown dimmed.
    Collapseddata-collapsedThe column is a narrow vertical strip; its list is hidden.
    Disableddata-disableddisabled or isItemDisabled: the grip is disabled and the card cannot move.

    Interaction behaviour

    • A press on the grip becomes a drag after the pointer moves 4px, so clicks and taps still work. The card lands in the column under the pointer, between the cards whose centres it passes.
    • Dropping outside every column, or pressing Escape, cancels and the card returns. Dropping on a refusing column also cancels and says why.
    • Near the edge of the board or a column, dragging scrolls it.
    • With dragFrom="card", the click that ends a drag does not reach the card, so the card is not opened by accident.
    • Keyboard: Space or Enter on the grip picks the card up; arrows move it; Space or Enter drops; Escape or Tab cancels. Focus stays on the card’s grip as it changes column. Collapsed and refusing columns are passed over.
    • A drop into a collapsed column (pointer or menu) moves focus to that column’s expand button.
    • onMove runs once per finished move, never while dragging.

    Keyboard support

    KeysAction
    TabMoves between grips, menu buttons, column toggles and the card content.
    Space / EnterOn a grip: pick the card up; again: drop it.
    ArrowUp / ArrowDownWhile picked up: move the card within its column.
    ArrowLeft / ArrowRightWhile picked up: move the card to the previous or next column that accepts it (mirrored in right-to-left).
    Home / EndWhile picked up: move to the top or bottom of the column.
    EscapeCancel the move (keyboard or pointer); the card goes back.
    Tab (while picked up)Cancels the move, then moves focus on.

    The "Move to…" menu follows the DropdownMenu keyboard model.

    Accessibility

    Role
    region (board, when labelled), group per column, list of listitems
    Accessible name
    Give the board an aria-label, such as "Sprint board". Each column is labelled by its heading, and its list by the same heading.
    • Each grip is a button named "Move {card}" and described by the instructions; aria-pressed is true while it is picked up.
    • An assertive live region announces pick-up, every move (column, position of total), drop, cancel, refusals and failed saves.
    • The header count has a text alternative: "3 cards, limit 2, over the limit".
    • The pointer copy is aria-hidden and inert; screen readers only meet the real card.
    • The "Move to…" menu is a single-pointer alternative to dragging (WCAG 2.5.7 Dragging Movements).
    • Column titles are headings (headingLevel, default 3) so people can jump between columns. Columns are labelled groups, not landmarks, so several boards on a page do not repeat landmark names.
    • Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/kanban-board.

    Responsive behaviour

    • Columns sit in one row that scrolls sideways; each is --mn-kanban-board-column-width (18rem) wide but never more than 85% of the board, so the next column peeks in on a phone.
    • On touch screens columns snap to their start when scrolled (not while dragging).
    • Give the board a height to make columns scroll on their own; otherwise they grow with their cards.
    • Touch drags from the grip, which has touch-action: none; the rest of the card scrolls normally.

    Theme support

    • Colours are Monarch semantic tokens only, so light, dark and more-contrast themes apply automatically.
    • Columns use --mn-color-bg-subtle, cards --mn-color-bg-surface with --mn-shadow-xs; the pointer copy uses --mn-shadow-lg.
    • The drop target uses --mn-color-accent-subtle and --mn-color-accent-strong; WIP tones use the warning and danger subtle/strong pairs.
    • Reduced motion removes transitions and the tilt of the pointer copy. Forced colours outline the drop target and placeholder with Highlight.

    Do and don't

    Do

    • Name the board with aria-label and give every card a short, unique getItemLabel.
    • Return the save promise from onMove so a failed save puts the card back and is announced.
    • Express workflow rules with canMove so refused columns are shown and skipped, rather than undoing moves after the fact.
    • Turn on moveMenu when cards are small or used on touch screens.

    Don't

    • Do not make the whole card the only way to move it: keep the grip, which is the keyboard path.
    • Do not put many columns of data on a card; open a detail view instead.
    • Do not hide business rules in the board: stage gates and side effects (such as asking for a reason) stay in the product, after onMove.
    • ReorderableList: One list to reorder, with the same keyboard model.
    • DataTable: Rows with many attributes to sort and compare.
    • DropdownMenu: The "Move to…" menu, and card actions inside renderCard.
    • Stepper: A fixed sequence of steps the user goes through once.

    Dependencies

    KindDepends on
    npm packagesreact
    Monarch componentsDropdownMenu

    Dragging uses pointer events only; no drag-and-drop library.

    --mn-kanban-board-column-width sets the column width (default 18rem).

    Testing coverage

    • Columns, headings, counts and WIP state render; axe passes.
    • Keyboard: pick up, move within and across columns, Home/End, drop; announcements at each step; focus stays on the moved card; right-to-left mirrors the arrows.
    • Escape and Tab cancel and restore the order; nothing is saved.
    • canMove and enforced WIP limits refuse columns, which are marked and skipped, with a "cannot move" announcement when none is left.
    • Optimistic moves: pending state while the promise runs, rollback and announcement on rejection, commit on resolve (uncontrolled), held until new columns (controlled).
    • Controlled boards apply onMove columns; the Move to menu moves to the end of a column, disables refused columns and restores focus.
    • Collapse (uncontrolled and controlled) with aria-expanded; keyboard moves skip collapsed columns.
    • Pointer: drag threshold, cross-column and within-column drops, cancel outside, on a refusing column and with Escape, drag from the card body but not from its controls or by touch, click suppression, drop into a collapsed column.
    • Disabled cards and board; renderCard state; translated messages; server rendering; drag helper maths.
    CheckCovered
    Axe in jsdomYes
    Server renderingYes
    Browser suitesVisual baselines: /__gallery/kanban-board; Cross-browser axe: /components/kanban-board

    Tests: packages/react/src/components/workflow/kanban-board