Columns of cards, one column per stage, where moving the card is the edit.
Part of the Konjo design language. The laws in the spine apply here unless this chapter contradicts them. Studio-only in practice — a board on a phone is a filtered list that has not been designed yet.
The gesture lives in direct manipulation; this chapter is the container it happens inside. They were written a round apart, and the gap between them was the point: a completeness test on a Leads board found the drag fully specified — three border states, a cursor table, a keyboard path, five announcement strings — sitting inside a column that had no width, no gap to its neighbour, and no place in the accessibility tree.
When a board
- The collection has a small, fixed set of stages the same records move through.
- Which stage a record is in is the fact you came for, more than any field on it.
- Moving something between stages is the work, not a side effect of it.
Otherwise use a table with a status column. A board with one busy column and five empty ones is a filtered list wearing five extra headers, and a board whose stages change per record is not a board at all.
Layout
GROW eyebrow
Leads display 34/800
Everyone who has asked about training here. body, text.secondary
↕24
NEW 12 CONTACTED 5 TRIAL SCHEDULED 3
─── sticky ───────────────────────────────────────────────────────
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Ana Torres ⠿ ⋮ │ │ … │ │ Nothing here yet.
│ Public page │ └──────────────────┘ │
│ 13 days │ ↕12 │
│ Call back Thu │ ┌──────────────────┐
└──────────────────┘ │ … │
↕12 └──────────────────┘
┌──────────────────┐
│ … │
└──────────────────┘
Show 25 more
│←─ 280 ─→│←24→│
| Value | |
|---|---|
| Column width | 280 fixed, minmax(280px, 1fr) — they grow only if all of them fit |
| Column gutter | spacing.s24 |
| Scroll | the board region is the one scroller, on both axes: it scrolls horizontally across stages and vertically within them, bounded to the viewport (max-height: calc(100vh − header)), and the page itself does not scroll on a board route. One scroller, never a scroller inside a scroller — this is also what makes sticky column headers possible, and what stops an off-screen stage with a hundred lost leads from setting the height of a page that shows four cards |
| Scroll cue | required, and measured — see tables. A board is the shape where this matters most: a column that is off-screen is a stage of the process the person cannot see, and unlike a table's fifth column they have no header row hinting that more exists |
| Column header | eyebrow text.secondary, count in caption text.secondary trailing, sticky on vertical scroll |
| Header → first card | spacing.s12 |
| Card → card | spacing.s12 |
| Volume | Reveal at 25 — Show 25 more / Show all N, below the last card in the column |
280, and the board scrolls. Six columns at 280 need 1800px and Studio is 1280–1440, so a six-stage board always scrolls sideways — which is correct and is how every board works. The alternative is dividing the width equally, and six equal columns at 1280 are 143px wide, which leaves a card about 111px of interior: a person's name wraps to three lines and the screen stops being scannable. A column is sized for its card, not for the viewport. If the stages do not fit, that is information about the stages.
Columns never reflow, stack, or collapse. The stage set is the screen; a board with fewer columns at a narrower width is a different screen showing different facts. This is the one place a Studio layout does not adapt, and it is deliberate.
Column order is the process order, never sorted by size or by activity.
The column
A column is not a panel and has no surface of its own — it is a header and a stack of cards
sitting directly on surface.page. This is forced, not chosen: cards are surface.card white,
so a white column behind them is 1.0:1, and surface.muted on surface.page is 1.07:1 —
the inversion L3 exists to kill. The column's bounds are implied
by the alignment of its cards, and that is enough.
No divider under the header. border.hairline on surface.page is 1.09:1; there is no rule
in this palette that reads on the page. Space does the work.
Cards are sorted, never hand-ordered. Sort by whatever answers the screen's question — usually longest-sitting first. This is not only a scanning decision: it means a drop into a column has exactly one target instead of N, so neither the pointer path nor the keyboard path needs an insertion index, and there is no drop-between affordance to design. If the product genuinely needs manual order within a column, that is a different chapter and a much larger one.
An empty column keeps its header and its 0 and prints one line of body 15/400
text.secondary, left-aligned, in flow: "Nothing here yet." No action, no illustration, never
centred in a void. Some columns being empty is the normal state of a pipeline, not a state to
recover from — and a column that vanished would change the shape of the process every day.
An empty board is six empty columns, not a separate empty block. The six lines already say it, and a block on top would put the same message twice in one viewport.
The card
| Surface | surface.card on surface.page, radius.card 12, spacing.s16 padding, no border at rest, no shadow |
| Height | content-driven, min-height never height |
| Subject | rowTitle 16/600 text.primary, wraps, never truncates |
| Supporting | caption 13/400 text.secondary, spacing.s4 under the subject |
| The next thing to do | caption 13/400 text.primary, spacing.s8 under the supporting line |
| Drag handle | iconSize.inline 16 text.tertiary, trailing on the subject's line |
| Overflow menu | iconSize.inline 16 text.primary, in a 32×32 target, after the handle |
The actionable line is darker than the metadata. A card whose "call back Thursday" is quieter than "Public page · 13 days" has inverted its own hierarchy — the metadata is why you are looking, the action is what you do.
A handle is required whenever the card contains a control, per direct manipulation: the handle drags, the body opens. It sits at the trailing end of the subject line so it never competes with the name for the reading position.
Three edge states without reflow. The card is 0 at rest, 1px border.control on hover
(Studio's pointer model — an object on the page gets an edge, not
a tint), and 2px border.control while dragged. Reserve the space with a transparent 1px
border at rest and swap its colour; draw the drag edge as an outline, which is outside the
box model and cannot shift anything. Never an inset shadow — L3.
No status pill unless the tone is decided. A board's cards are already sorted and positioned by their status; a pill that repeats the column is the same fact twice. A pill for something else — an SLA breach, a flag — needs a tone, and a tone needs a threshold, which is a product fact. Until it is answered, the fact renders as plain text.
States
| Loading | Header, counts and column headers render immediately; three card-shaped skeletons per column — surface.card at radius.card with border.input bars inside. No pulse. Hidden from assistive tech; completion announced once for the page |
| Empty column | above |
| Not yet set up | one body line and one text link, never dressed as data or as an error. See states |
| One column fails | the header and count stay; the body carries an inline error block and a bordered secondary retry at controlHeight.dense 36. The other columns keep rendering |
| The board fails | nothing else is on screen, so the retry is the filled primary at controlHeight.primary 48 |
| A move fails | the card returns to its exact origin slot, and an inline block states it at the top of the board region, full width — never a toast |
| Someone else moved it | the card goes where the server says and the announcement says so. Never silently |
Accessibility
Most of this is the web contract and direct manipulation; these are the board-specific parts.
- The board is a
role="group"with anaria-label, holding one<section>per column, each labelled by its<h2>and described by its count. ARIA has no board role; a labelled group of labelled regions is what a screen reader can actually navigate. <h1>is the page,<h2>is every column. The heading list is how someone gets the shape of the process.- The board is one tab stop, roving
tabindex, arrows to move — a composite widget, per the composite-widget rule. Forty cards is not forty tab stops. - A card carries
aria-roledescription("lead card") and anaria-labelthat reads the whole card: subject, source, dwell, next action. Everything drawn on it, in one string. - The in-card controls need a keyboard route out of the one tab stop.
Shift+F10(and the context-menu key) opens the card's menu; that is what those keys are for and it is the only path that does not break the roving model. - Two live regions, polite for progress and assertive for outcomes — see direct manipulation.
- A board has no alert slot. The urgent thing is one card among forty, and L4 rules that a shape with no slot spends no emphasis. Sorting is what surfaces urgency here.
The self-check
- At the narrowest supported width, is a person's name still on one or two lines?
- Does every column say something when it is empty, including the ones that are usually empty?
- Can someone who cannot drag move a card in the same number of steps?
- Is the card's most actionable line its darkest one?
- Is anything on the card repeating the column it is already in?