Studio's first-class layout. Rows an owner scans, sorts, filters and acts on in bulk.
Part of the Konjo design language. The laws in the spine apply here unless this chapter contradicts them. Studio-specific — see the dialect.
A table is not a list with lines. The mobile list row is designed for one thumb and one thing at a time; a table is designed for a pointer, a wide screen, and a person comparing forty records against each other. Tables are Studio-only. A table in the mobile app is a list that has not been designed yet.
When a table
- The person is comparing across rows — who has not paid, which classes are under-attended.
- The data is genuinely columnar and every row has the same fields.
- There are more than ~10 rows, or bulk action is the point.
Otherwise use cards. Six records with four fields each is not a table, it is six cards, and the table's chrome costs more than it returns.
Anatomy
MEMBERS eyebrow, text.secondary
Students display, page title [ Add member ]
Everyone training at Blue Heron. body, text.secondary
┌────────────────────────────────────────────────────────────────────────┐
│ [ ⌕ Search students… ] Export CSV │ toolbar, inside
│ ( All ) ( Brown belt ×) ( Past due ) 132 students │ chips, then hairline
│ ═══════════════════════════════════════════════════════════════════════ │
│ ☐ NAME ▲ RANK LAST SEEN STATUS │ header: eyebrow
│ ─────────────────────────────────────────────────────────────────────── │
│ ☐ Ana Torres ● Brown Aug 21 Waiver missing │ rows 46 min
│ ☐ Marcus Webb ● Green Today │
│ ─────────────────────────────────────────────────────────────────────── │
│ Show 25 more Show all 132 │ reveal, in-panel
└────────────────────────────────────────────────────────────────────────┘
- White card on the page, hairline rows, no vertical rules. Vertical lines make a table look like a spreadsheet and add nothing a column gap does not.
- Search, filter chips and the table share one panel — one surface around the whole
apparatus, not a floating toolbar above a separate card. Filters that sit outside the panel
read as page furniture; a person cannot tell whether they filter this table or the page.
Toolbar padding is
12vertical /16horizontal; the chip row closes with a hairline; the table starts flush against it with no surface of its own. - What makes it a panel is the tone, not the border. White
surface.cardon the tintedsurface.page, per L3. This chapter said "one bordered panel" and that reads as a contradiction of L3's "not this: a 1px border around white on white" — it isn't, but only because the page is tinted.border.hairlineon the page is 1.09:1: an optical edge that crisps the corner radius, never the thing doing the separating. If you find yourself reaching for a heavier border to make a panel read, the panel is on the wrong surface. - The page's own actions stay outside the panel, on the title row — Add member, Import. Those act on the roster, not on this view of it. Actions that act on what you are looking at (Export CSV, column chooser) belong in the toolbar.
- Rows are
46minimum — Studio is denser than mobile by design. Minimum, not fixed: see density for why the row yields rather than clips. - Header is an
eyebrowregister,text.secondary. The budget counts registers, not instances, so a page eyebrow above the title and a table header are the same register and both are legal — the standard Studio page header (eyebrow, title, one-line sub) is not in conflict with a table below it. What the budget forbids is a second uppercase style: a capsstatement, asectionLabel, or capitals typed by hand. - The header sticks on vertical scroll. A column you cannot name is a column you cannot read.
- No zebra striping. Hairlines already separate rows; stripes add a second separator and a second background, and the design has one background.
- Dense grading grid — the one deviation. Class Mode's curriculum grid
(
OrganizerGrid, students × techniques) is read along both axes: down a technique, and across a student's row through a dozen tappable cells. A column gap cannot hold the eye across the middle of a grid that wide, so that grid alone drawsborder.hairlinevertical rules between columns and closes every fifth row (rows 5, 10, 15…, never the last) on aborder.inputrule, name cell included, so the line runs the full width. Stripes stay banned there too — the grouping rule does the job a stripe would, without a second background. A roster or list table is read one row at a time and gets neither. - Row hover gets
surface.mutedonsurface.card— 1.04:1, and the surface matters: the same token onsurface.pageis 1.07:1, so this rule only holds for a row inside a panel. Pointer input has hover; use it. It is the cheapest possible affordance and it tells a person which row their action will hit. surface.mutedis never the only signal of anything.surface.muted on surface.cardis 1.04:1 — the same arithmetic L3 cites when it kills the grey-card-on-white inversion. It works for hover because the pointer is already sitting on the row and is the real signal; the fill is confirmation, not information. It does not work alone for selection, which is a component state and owes 3:1 under SC 1.4.11. What carries selection is the checkbox —border.controlat 3.53:1 on the card, with a realchecked/aria-checked— and the row fill is the hint beside it. A "selected" row with no checkbox in it is a bug, not a style.
Columns
- The subject is the first column, always, and it is the only bold one. A name should never be the third thing on a row.
- Numbers are right-aligned and tabular; text is left-aligned. Never centre a column.
- Units go in the header, not in every cell —
REVENUE ($), then45.00. - Five to seven columns. Past that, a person is scrolling horizontally to compare things they wanted side by side; move the rest to the detail view.
- The last column is status or the row action, never data someone needs.
- Status renders as a pill, and a pill is not a chip. A chip is a control you click; a pill
states a fact and does nothing. 24 tall, 8 horizontal,
radius.pill,caption, the matchingfeedback.*fill and text, and no border — 3:1 is owed by a control boundary, and a pill is not tappable. It never has a hover state. Geometry in Studio metrics. - Never truncate a name. Give it the width; truncate a description instead. See content format.
Sorting
- One sort at a time, indicated by a small caret in the header, on the sorted column only.
- Sortable headers look tappable — they get a hover state; unsortable ones do not.
- The default sort answers the screen's question: most recent for activity, soonest for what is upcoming, alphabetical only when nothing better exists.
- Sorting does not lose the selection.
Selection and bulk action
- Checkbox column first, header checkbox selects the filtered page and says so. It is a
16pt box whose click target is the row's full height, and when some but not all rows are
checked it is
aria-checked="mixed"— a plain checked box on a partial selection is lying about the state. - A selected row fills
surface.mutedonsurface.cardand keeps it while hovered; hover on an unselected row is the same fill, so a row that is both looks selected, which it is. Never a coloured selection fill — the red role is not spent on a state a person can toggle by accident. - The bulk action bar replaces the filter row when anything is selected, states the count — "3 students selected" — and offers at most three actions.
- Bulk destructive actions confirm and name the count: Remove 3 students? See serious actions.
- Escape clears the selection.
Density and volume
Never render an unbounded list, and never infinite-scroll. (System-wide, not table-local: a board column, a feed and a card list obey this and both shapes below.) Infinite scroll in a work tool loses a person's place and makes "how many are there" unanswerable. There are exactly two sanctioned shapes, and which one a table uses is decided by where the data comes from, not by taste:
Reveal Cursor page Use when the whole collection is already loaded and filtering must be instant — a roster, a team list the collection is unbounded or too large to preload — signatures, payments, audit history First render 25 rows 50 rows Control Show 25 more·Show all 132Next 50 →·← First pagePlacement inside the panel, below the last row inside the panel, below the last row On filter change resets to 25 resets to the first page Round-trip none — reveal is a render decision one per page Reveal is the default; reach for cursor paging only when preloading the collection is not defensible. Both keep the same control placement, so the two shapes never look like two different products.
"No pagination" in the screen inventory means no round-trip per page — it describes a Reveal table, not an endless one.
The count is always visible: "132 students", and when filtered, "14 of 132". A Reveal table also states what is on screen when the two differ — "showing 25 of 132".
Column widths are stable across pages. Content-driven widths that shift every page make a table impossible to scan.
Horizontal scroll is contained to the table, never the page.
A region that scrolls sideways must say so, and the cue must be driven by actual overflow — measured — not by a proxy for it.
overflow-x: autogives no affordance of its own: on a trackpad or a phone there is no visible scrollbar at rest, so a clipped column is indistinguishable from a column that does not exist.This is stated because a blind QA pass found three Studio surfaces answering it three different ways: the roster measures its own right-edge overflow and floats a
Scroll →chip; the waiver matrix prints a hint when there are more than three templates, which is a proxy for width and fails at a narrow viewport with two; and the board and style pages print nothing. The proxy is the instructive failure — the grid still overflowed, and the hint stayed silent because the condition was counting the wrong thing.The cue is a chip over the content, not a fade. L3 bans gradients, which rules out the usual edge-fade; and a fade says "there is more" without saying which way. The chip carries a direction, is
captiononsurface.tag, sits over the rows rather than on the header line, isaria-hidden(a screen reader reaches the columns regardless), and disappears once the region is scrolled to that end.
States
- Loading: the header and column structure render immediately; rows are skeletons at row height. Never a spinner where the table goes.
- Empty collection: replace the table with an empty state — and drop the filter row, since there is nothing to filter.
- Empty after filtering: keep the table header and the filters, and say which filter is excluding everything. See search and filter.
- Error: an inline block above the table with a retry. The columns stay.
Accessibility
- Real semantic table markup:
<table>,<thead>,<th scope="col">. A grid of<div>s is unnavigable by screen reader. aria-sorton the sorted header.- Every row action has a label naming its row — "Remove Ana Torres", not "Remove".
- Full keyboard operation: tab reaches every control, the header sorts on Enter, Escape clears.
- Focus visible at 2px
border.focus, which on a dense table is the only way a keyboard user keeps their place.
Never
- A table on mobile.
- Vertical rules, or zebra stripes. (Vertical rules only in the dense grading grid, above.)
- Centred columns.
- A truncated name.
- Infinite scroll.
- Row actions with no row named in the label.
- More than one uppercase register on a page that already has a table header.