<!-- https://getkonjo.com/design/patterns/board · source: docs/design/patterns/board.md -->

# Boards

Columns of cards, one column per stage, where moving the card *is* the edit.

> Part of the [Konjo design language](../konjo-design-language.md). 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](direct-manipulation.md); 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](../studio/tables.md) 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](../studio/tables.md#density-and-volume). 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](../konjo-design-language.md). 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](direct-manipulation.md): 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](../studio/metrics.md#pointer) — 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](../product-facts.md). 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](states.md) |
| 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](../foundations/accessibility.md) and
[direct manipulation](direct-manipulation.md); these are the board-specific parts.

- **The board is a `role="group"` with an `aria-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](../foundations/accessibility.md). Forty cards is not forty tab stops.
- **A card carries `aria-roledescription`** ("lead card") and an `aria-label` that 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](direct-manipulation.md).
- **A board has no alert slot.** The urgent thing is one card among forty, and
  [L4](../konjo-design-language.md) 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?
