Kanban board. Columns come from columns, cards from cards, grouped by the field groupBy names. A card moves three ways, pointer drag with an insertion line, a keyboard lift with every step announced, and the move() method, and all three raise one cancelable suiMove. Reach for it wherever a team stands in front of its work by stage.
Each column is a listbox of option cards with one tab stop. Drag a card, or focus one and press Space to lift it: every keyboard step is announced in a live region, and one cancelable suiMove fires on the drop.
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#bd-basic');
el.columns = stages();
el.cards = work();
}
Limits and tones
A column limit shows in the count and turns to the attention tone once the count passes it; the board still allows the move, so a host that wants a hard limit refuses suiMove. A column tone draws the status dot in the ladder's five shapes.
<sui-board id="bd-limits" aria-label="Launch board with limits" style="height:420px"></sui-board>
variant plain puts the headings, counts and cards straight on the ground, rules each heading with a hairline and shows the collapse control on hover and focus only. density runs compact, standard and comfortable: compact pads a card 8 by 10 at a 6px gap, so about seven cards show where five did.
<sui-board id="bd-plain" aria-label="Launch board, plain and compact" variant="plain" density="compact" style="height:420px"></sui-board>
collapsed lists the columns drawn as strips; a strip still takes a drop at its end and, since the card lands out of sight, focus moves to its expand button. A keyboard lift skips strips. An empty column shows emptyMessage at rest; an empty string keeps the dashed target and drops the words.
No cards match the filter.
<div style="display:grid;gap:10px">
<sui-checkbox id="bd-once" label="Say it once above the board"></sui-checkbox>
<p id="bd-once-line" hidden style="margin:0;font:460 13px/1.5 var(--font-sans);color:var(--ink-muted)">No cards match the filter.</p>
<sui-board id="bd-collapsed" aria-label="Launch board, two columns collapsed" empty-message="No cards" style="height:400px"></sui-board>
</div>
cardRender draws the card content from the card and its column; the board keeps the drag, the keyboard lift and the focus. Return an element to compose library parts such as sui-avatar, built with root.ownerDocument so the same code runs on a server.
<sui-board id="bd-custom" aria-label="Launch board with owners" style="height:420px"></sui-board>
loading draws three skeleton cards per column. no-drag removes the pointer drag and the insertion line for a touch surface where a long press means something else; the keyboard lift and move() remain, so drag is never the only path.
suiMove fires once per move from a drop, a keyboard lift or move(), with the card, both columns, both positions and whether the target is a collapsed strip. It is cancelable: here a Blocked card is refused a move into Done and stays put. suiCollapsedChange carries the collapsed ids and suiCardClick the opened card.
Move WS-101 to ReviewNothing moved yet.
<div style="display:grid;gap:10px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<sui-button id="bd-move" variant="outline" size="sm">Move WS-101 to Review</sui-button>
<code id="bd-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing moved yet.</code>
</div>
<sui-board id="bd-events" aria-label="Launch board with a log" style="height:400px"></sui-board>
</div>
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#bd-events');
const log = root.querySelector('#bd-log');
el.columns = stages();
el.cards = work();
el.addEventListener('suiMove', (e) => {
const { id, from, fromIndex, to, index, collapsed } = e.detail;
const card = el.cards.find((c) => c.id === id);
if (to === 'done' && card.pill && card.pill.label === 'Blocked') {
e.preventDefault();
log.textContent = `Refused: ${id} is blocked and cannot move to Done.`;
return;
}
log.textContent = `${id} moved from ${from} (${fromIndex + 1}) to ${to} (${index + 1})${collapsed ? ', which is collapsed' : ''}.`;
});
el.addEventListener('suiCollapsedChange', (e) => {
log.textContent = e.detail.length ? `Collapsed: ${e.detail.join(', ')}.` : 'Every column open.';
});
el.addEventListener('suiCardClick', (e) => {
log.textContent = `Opened ${e.detail.id}: ${e.detail.card.title}.`;
});
root.querySelector('#bd-move').addEventListener('click', async () => {
const moved = await el.move('WS-101', 'review');
if (!moved) log.textContent = 'WS-101 is already in Review.';
});
}
Keyboard
Key
Action
Tab
Steps through each column: its collapse button, then the card that holds that column's tab stop.
Space
Lifts the focused card, and drops a lifted card where it is.
Enter
Opens the focused card (suiCardClick), and drops a lifted card.
ArrowDownArrowUp
Moves the tab stop to the next or previous card in the column. With a card lifted, moves the card one place.
ArrowRightArrowLeft
Moves the tab stop to the current card of the next or previous open column. With a card lifted, carries it into that column at the same position. Collapsed columns are skipped.
HomeEnd
Moves to the first or last card in the column. With a card lifted, moves it to the top or bottom.
Escape
Cancels a lift and returns the card to where it started.
Draws a card's content (card, column) => string | HTMLElement; strings render as text. Without it the card shows title, subtitle, meta and pill. A callback that throws costs only its card, which draws the built-in body, and the error is logged once. Property, not attribute.
cards
property only
SuiBoardCard[]
[]
Cards, in the order they sit within their columns. Mutable: a completed move writes the card back with its new groupBy value at its new position, so a host that keeps its own copy updates it in suiMove. A card whose group matches no column is not drawn. Property, not attribute.
collapsed
property only
SuiKey[]
[]
Ids of the collapsed columns, each drawn as a narrow strip that still takes a drop. A card that lands in one is out of sight, so focus moves to the strip's expand button. Mutable; updated before suiCollapsedChange fires. Property, not attribute.
columns
property only
SuiBoardColumn[]
[]
Columns in order. Each holds the cards whose groupBy field equals its id. Property, not attribute.
density
density
"comfortable" | "compact" | "standard"
'standard'
Card spacing, on the scale the table, the grid and the list use. standard, the default, pads a card 10px by 12px with 8px between cards. compact takes the padding to 8px by 10px and the gap to 6px, so about seven cards show where five did; the title, subtitle, meta and pill all stay. comfortable pads 14px with 10px between cards.
emptyMessage
empty-message
stringstring | undefined
Text an empty column shows at rest. An empty string keeps the dashed target and drops the words, for a host that says it once above the board. While a card is in flight the column reads as a drop target instead.
groupBy
group-by
string
'status'
The card field that names its column. The board writes the target column's id into it on every move.
loading
loading
boolean
false
Draws three skeleton cards per column instead of the cards, and no counts. Reflected, so a page can style the wait.
noDrag
no-drag
boolean
false
Turns off the pointer drag and its accent insertion line. The keyboard lift and move() remain, so drag is never the only path. Reflected.
variant
variant
"card" | "plain"
'card'
Column chrome. card draws each column as a sunken, bordered well with its collapse control always showing; plain puts the headings, the counts and the cards straight on the ground, rules each heading with one hairline, and shows the collapse control on hover and focus of the heading only. A collapsed strip keeps its dashed edge while a card is over it either way.
Events
Event
Detail
React
Vue
Description
suiCardClick
SuiBoardCardClickDetail
onSuiCardClick
@sui-card-click
Fires on a click or Enter on a card that is not lifted; detail is { id, card }.
suiCollapsedChange
SuiKey[]
onSuiCollapsedChange
@sui-collapsed-change
Fires when a column collapses or expands; detail is the new collapsed. Does not bubble.
suiMove
SuiBoardMoveDetail
onSuiMove
@sui-move
Fires before a card moves, from a drop, a keyboard lift or move() alike; detail is { id, from, fromIndex, to, index, collapsed }, where collapsed says the card is landing in a strip. Cancelable: preventDefault() refuses the move and the card stays where it was. Otherwise the board writes cards and moves the card.
Methods
Method
Signature
Description
move()
move(id: SuiKey, to: SuiKey, index?: number) => Promise<boolean>
Moves a card to column to at index (the end when omitted) through the same cancelable suiMove a drop raises. Resolves true when the card moved, false when the id or column is unknown, the card is already there, or a listener refused it.
setFocus()
setFocus() => Promise<void>
Moves keyboard focus to the board's first tab stop: the current card of the first open column that has one.
Types
SuiBoardCardinterfaceA card. The default renderer draws title, subtitle, meta and pill; the field named by groupBy places it in a column, and any other field is the host's to read in cardRender.
id
SuiKey
Identity, unique on the board, kept as given: a numeric id stays a number.
title
string
subtitle?
string
meta?
string
pill?
SuiBoardPill
SuiBoardCardClickDetailinterfacesuiCardClick detail: the card's id and the card.
id
SuiKey
card
SuiBoardCard
SuiBoardColumninterfaceOne column. It holds every card whose groupBy field equals its id.
id
SuiKey
The group value this column holds; a card lands here when card[groupBy] equals it, compared as text so a numeric status id matches. A move writes it into the card as given.
heading
string
Heading text.
limit?
number
Work-in-progress limit. The count reads n / limit and takes the attention tone past it. The board still allows the move; a host that wants a hard limit refuses suiMove.
tone?
SuiSeverity
Status dot beside the heading, drawn by sui-badge in the ladder's five shapes.
SuiBoardMoveDetailinterfacesuiMove detail: one card, where it was and where it is going.
id
SuiKey
The moving card's id.
from
SuiKey
Column id the card leaves.
fromIndex
number
Its position in that column before the move, from zero.
to
SuiKey
Column id the card lands in; the same as from for a reorder.
index
number
Its position in that column after the move, from zero.
collapsed
boolean
True when that column is a collapsed strip, so the card lands out of sight and focus moves to the strip's expand button rather than to the card.
SuiBoardPillinterfaceThe pill on a default card, painted by sui-tag from the status ladder.
label
string
severity?
SuiSeverity
SuiKeytypeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiRenderResulttype
No members.
SuiSeveritytype
No members.
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.