Browse pages
List and Table Patterns
A list or a table shows many records of one kind, so a reader can find, compare, and act on them.
Intent#
A list or a table shows many records of one kind, so a reader can find, compare, and act on them. These patterns answer how columns, sorting, filters, selection, paging, and the states of a table behave. Each pattern offers drawn options and names a default.
Standards#
Keep a list's view in its address#
ruleguidanceRequirement A list MUST keep its filters, sort order, and page in the URL.
Rationale A reader who opens a record and comes back finds the same view. A colleague who receives the link sees the same rows.
Align money to the right with its currency#
ruleguidanceRequirement A table MUST right-align a money column and show the currency of every amount in it.
Rationale Right-aligned figures line up by place value, so a reader compares amounts down the column. An amount without its currency is a number the reader has to interpret.
Patterns#
Choosing the columns#
patternguidanceQuestion Which columns does an orders table show. The reader chooses rows by order, customer, date, state, and money. The delivery rides under the order code.
Default five-columns-nothing-removable
Compact The recommended option reflows below md. One column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the default five columns. They answer which order, who, when, what state, and how much. The runner-up is the Columns popover. Add it once more than one team asks for a sixth fact such as method or channel.
Saved views are the later answer, when finance and the warehouse want different tables. Never ship the question-shaped headers. They cost width and read as a form.
five-columns-nothing-removableFive columns, nothing removableDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text muted "Garcia Interiors restock" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The default set. Five columns is what fits 1440 without scrolling and covers the four questions: which order, who, when, what state, and what money.
Cost Nothing can be dropped or added. A team that wants the delivery as its own column, or that hides the email, has no way to get there.
six-columns-with-a-column-chooserSix columns with a column chooserpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "State" select = "Any state"
action "Columns"
region "Orders"
table 6 "Order" "Delivery" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Garcia Interiors restock" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Add the delivery as its own column and offer a chooser so each back-office user keeps the columns they use. TanStack draws this as a plain per-user map, so the state is cheap.
Cost The chooser is one more control, and a table where everyone sees different columns is harder to talk about in a support reply.
Reference TanStack
a-chooser-that-shows-the-current-setA chooser that shows the current setpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
action "Columns"
split 3:1
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
aside "Columns"
field "Order" checkbox
field "Customer" checkbox
field "Placed" checkbox
field "State" checkbox
field "Paid" checkbox
field "Delivery" checkbox
field "Method" checkbox
field "Channel" checkbox
action quiet "Reset"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The Columns control opens a list of every column with a checkbox, a drag handle, and a Reset to default. The reader sees what they are looking at rather than picking from memory.
Cost Two panes, so the table is squeezed while the chooser is open. A popover narrow enough to fit beside the button avoids that.
two-lines-per-row-one-column-per-fact-groupTwo lines per row, one column per fact grouppage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 4 "Order and delivery" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text muted "Garcia Interiors restock" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Fold delivery into the order cell and customer into one cell. The table is three columns wide and each row reads as two records of two lines.
Cost Sorting by delivery becomes impossible, because delivery is not a column. Carbon wants sorting in the headers, and a folded cell has nowhere to put an arrow.
columns-sized-to-their-contentColumns sized to their contentpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text muted "Garcia Interiors restock" · text "Maria Garcia" · text muted "maria@garcia-interiors.example" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The money column is narrow and right-aligned, the customer column takes the slack, the date column is fixed. Each column is as wide as its values rather than an even split.
Cost A long customer name pushes its column wider on one row and narrower on the rest, because table layout is content-driven. Fixed widths are the only way to stop that.
one-column-per-question-headers-that-name-the-questionOne column per question, headers that name the questionpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "State" select = "Any state"
field "Delivery" select = "Any delivery"
region "Orders"
table 5 "Who bought" "For which delivery" "When they paid" "What state" "How much"
item: text "Maria Garcia" · text "Garcia Interiors restock" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Headers become the questions the reader asks: Who, Which delivery, When, State, How much. Each header is sortable, and the reader can name what they are looking at.
Cost Longer headers take width from the data. A question-shaped header breaks the one-word-per-column feel.
customer-first-order-code-deferred-to-the-recordCustomer first, order code deferred to the recordpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Three orders for Garcia Interiors restock, newest first."
toolbar
field "Customer name" search
field "State" select = "Any state"
action "Export"
region "Orders"
table 4 "Customer" "Delivery" "State" "Paid"
item: text "Maria Garcia" · text "Garcia Interiors restock" · status "Paid" positive · text "EUR 125.00" · action "Open"
footer
text "1 – 3 of 3"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Support starts from who is asking, not from a code. The customer and the delivery lead, and the order code waits on the record where it is needed.
Cost Two orders by one customer for one delivery read as near-identical rows until one is opened. Nothing on the row tells them apart, which costs the scannable identity the default order column gives.
a-columns-popover-listing-every-columnA Columns popover listing every columnpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
action "Columns"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
popover "Columns"
field "Order · Always shown" checkbox
field "Customer" checkbox
field "Placed" checkbox
field "State" checkbox
field "Paid" checkbox
field "Delivery" checkbox
field "Method" checkbox
field "Channel" checkbox
action quiet "Reset to default"
Rationale The Columns control opens a popover with every column, a checkbox each, and Reset to default. The order column is always shown and says so. This is the shadcn/ui data-table view-options menu, and Linear Display panel works the same way.
Cost The popover covers the table while it is open, and drag-to-reorder inside it is a second feature that most back-office users would never find.
Reference shadcn/ui
saved-views-carry-their-own-columnsSaved views carry their own columnspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Finance view: what was paid, how, and what went back."
toolbar
field "Search orders" search
field "Date" select = "This month"
tabs "All orders" "Finance" "Dispatch list"
region "Finance"
table 5 "Order" "Placed" "Method" "Paid" "Refunded"
item: text "7K4Q2M" · text "8 Oct 09:32" · text "Bank transfer" · text "EUR 125.00" · text muted "No refund"
footer
text "1 – 25 of 58"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale HubSpot and Linear tie a column set to a saved view. All orders keep the default five, Finance swaps in method, paid, and refunded, and Dispatch list shows customer, delivery, and lines. Each role opens the table it needs, and a support reply can name the view.
Cost Views are a feature with their own lifecycle: who creates one, whether shared, and what follows when a column retires. Too much for a first release.
Reference HubSpot
Sorting#
patternguidanceQuestion How does a reader reorder an orders table. Newest first is the start. Each sortable header offers its own order, and the sort lives in the URL so a link carries it.
Default every-sortable-header-carries-an-arrow
Compact Below 640 pixels the table becomes two-line rows and the headers leave with it. A Sort control carries the same sort from the toolbar. The compact option is headers-on-a-desktop-a-sort-control-on-a-phone.
Rationale Give every sortable header its own arrow. The arrow states that headers sort on touch screens too. The runner-up is headers on a desktop with a Sort control on the phone list. It becomes necessary the moment the table turns into rows below 640 pixels.
Never ship multi-column sort. No back-office question needs two keys.
arrow-on-the-sorted-column-onlyArrow on the sorted column onlypage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Sorted by newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed ↓" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale One arrow, on the column the table is sorted by, showing which way. Every other header is plain text that becomes a button on hover.
Cost Nothing tells a reader that the other headers sort at all until they hover, which is invisible on a touch screen.
sort-as-a-control-above-the-tableSort as a control above the tablepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
field "State" select = "Any state"
field "Sort" select = "Newest first"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The sort is one named control rather than six header affordances: Sort newest first. Works on a touch screen and states itself in words.
Cost The reader has to find the control to change the order, and Carbon guidance states that sorting belongs in the headers.
arrows-on-hover-plus-a-resetArrows on hover plus a resetpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
action "Reset sort"
region "Orders"
table 5 "Order ↕" "Customer ↕" "Placed ↓" "State ↕" "Paid ↕"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Every header carries a faint arrow, and the sorted one is dark. A Reset sort control appears only while a sort is applied, so the default state stays clean.
Cost The reset is a control that only sometimes exists, which is its own inconsistency. It is also invisible until a sort has been applied.
multi-column-sort-shown-as-a-stackMulti-column sort, shown as a stackpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
action "Sort: 2"
region "Orders"
text "Sorted by delivery, then by date inside each delivery."
table 5 "Order ↑ 1" "Delivery" "Customer" "Placed ↓ 2" "Paid"
item: text "7K4Q2M" · text "Garcia Interiors restock" · text "Maria Garcia" · text "8 Oct 09:32" · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale When a reader sorts by delivery and then by date, the table says so in words rather than relying on two arrows. Useful once a table genuinely needs a secondary key.
Cost Almost no reader needs two keys, and the control is a menu of menus. Carbon treats sorting as one column at a time.
sorting-stated-in-the-page-descriptionSorting stated in the page descriptionpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first. Click a heading to sort it."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The order is stated once, in words, under the heading, and the headers stay plain. A reader who wants a different order clicks a header.
Cost The statement goes stale the moment a reader sorts by clicking, so the page contradicts itself unless the description updates too.
sort-only-the-column-that-has-choicesSort only the column that has choicespage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
region "Orders"
text muted "Only date and money sort."
table 5 "Order" "Customer" "Placed ↓" "State" "Paid ↕"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Only two headers are buttons: money and date. Order and state sort alphabetically, which almost nobody wants, so they are not buttons at all.
Cost Selective sorting breaks the expectation that every header is a control, and it hides a capability rather than offering it.
every-sortable-header-carries-an-arrowEvery sortable header carries an arrowDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
region "Orders"
table 6 "Order ↕" "Customer" "Placed ↓" "State ↕" "Paid ↕"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Each sortable header is a ghost button with a faint up-down arrow, and the sorted one has a solid arrow. The shadcn/ui and TanStack table examples do the same, and so does Stripe.
Cost Five arrows is more ink in the header row, and the Customer header looks inert beside its neighbors.
Reference shadcn/ui
headers-on-a-desktop-a-sort-control-on-a-phoneHeaders on a desktop, a sort control on a phonepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
field "Sort" select = "Newest first"
region "Orders"
table 6 "Order ↕" "Customer" "Placed ↓" "State ↕" "Paid ↕"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Compact layout:
page
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery."
toolbar
field "Search orders" search
field "Sort" select = "Newest first"
region "Orders"
list 6
item: text "7K4Q2M · Garcia Interiors restock" · text "EUR 125.00" · text muted "Maria Garcia · 8 Oct 09:32" · status "Paid" positive
one-filter-per-category-visibleOne filter per category, visibleDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "268 orders across 40 deliveries."
toolbar
field "Search orders" search
field "Delivery" select = "Any delivery"
field "State" select = "Any state"
field "Date" select = "Any date"
field "Channel" select = "Any channel"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Stripe sorts from the headers on a wide screen. Its mobile list carries a Sort control instead, because rows have no headers to click. The same URL parameter drives both.
Cost Two controls for one state, and only worth building once the table becomes a list on a phone.
Reference Stripe
Filters#
patternguidanceQuestion How does a reader narrow an orders table. Search alone answers one question. Each closed filter shows how many are applied, and several categories never share one dropdown.
Default one-filter-per-category-visible
Compact The recommended option reflows below md. One column, header actions under the title, and a table scrolls inside its own region.
Rationale Show one visible control per category. A reader sees what can narrow the table without opening anything, and each category keeps its own control. The runner-up is faceted buttons with counts. Prefer it once a category holds more than a few values and the reader needs the split before choosing.
Never ship every category inside one dropdown. It hides the applied state and contradicts the stated rule.
applied-filters-restated-as-chipsApplied filters restated as chipspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "12 orders match."
toolbar
field "Search orders" search
field "Delivery" select = "Garcia Interiors restock"
field "State" select = "Any state"
field "Date" select = "This month"
action quiet "Clear all"
row
action quiet "Garcia Interiors restock x"
action quiet "This month x"
region "Orders"
table 4 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 12 of 12"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Delivery, state, date, and channel each get their own control in one row. No menu holds more than one category, so a reader can see what is available without opening anything.
Cost Four controls plus a search box is a wide toolbar. Below about 700 pixels they have to wrap or collapse, which is where the complexity moves.
filters-with-an-applied-count-on-the-controlFilters with an applied count on the controlpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "268 orders across 40 deliveries."
toolbar
field "Search orders" search
field "Delivery" select = "Garcia Interiors restock"
status "1" info
field "State" select = "Paid"
status "1" info
field "Date" select = "Any date"
status "0" neutral
action quiet "Clear 2"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The controls stay, and the row underneath restates what is applied with a remove control each. The chip row is the honest answer to what the reader is looking at, which the toolbar alone is not.
Cost The chips restate the controls, saying one thing twice on one page.
a-filter-panel-beside-the-tableA filter panel beside the tablepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "268 orders across 40 deliveries."
split 1:3
aside "Filter"
field "Search" text
field "Delivery" select = "Any delivery"
field "State" select = "Any state"
field "Channel" select = "Any channel"
field "Only refunds" checkbox
action primary "Apply"
action quiet "Clear"
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Each closed control carries a count badge, so the reader knows a filter is narrowing the table without opening anything. Carbon asks for this minimum indicator.
Cost A count of one on a select reads as decoration unless the reader knows what it means. It is only clear beside a label.
filters-in-one-dropdownFilters in one dropdownpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "268 orders across 40 deliveries."
toolbar
field "Search orders" search
action "Filters (2)"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Filters in a panel down the left side, with every category open at once and nothing hidden. Carbon prefers this when there are more than three categories.
Cost A permanent panel takes about 190 pixels from the table on every page, and most visits apply no filter at all. The panel is a cost paid by everyone for the benefit of a few.
Reference Carbon
views-as-tabs-filters-as-controlsViews as tabs, filters as controlspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
tabs "All 268" "Needs attention 12" "Awaiting money 31" "Refunded 18"
region "All orders"
toolbar
field "Delivery" select = "Any delivery"
field "State" select = "Any state"
field "Date" select = "Any date"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale One Filters button holding every category, with the count on the button. Keeps the toolbar to two controls and leaves the table full width.
Cost Carbon states that multiple filter categories never sit inside a menu. A reader cannot see that a date filter exists without opening it, and closing it hides the applied state again.
every-option-carries-its-count-before-it-is-appliedEvery option carries its count before it is appliedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "58 of 268 orders match."
toolbar
field "Search orders" search
field "Delivery" select = "Garcia Interiors restock (41)"
field "State" select = "Any state (268)"
field "Date" select = "This month (58)"
action quiet "Clear all"
message "Each count answers the other filters as set." info
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 58"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The tabs carry the question the reader arrived with: All, Needs attention, Awaiting money. The filters below refine whichever tab is open.
Cost Two mechanisms over one table, and the reader has to work out whether to click a tab or a filter. HubSpot makes views do both jobs instead.
faceted-filter-buttons-with-countsFaceted filter buttons with countspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "58 of 268 orders match."
toolbar
field "Search orders" search
action "State: Paid, Awaiting payment"
action "Delivery: Garcia Interiors restock"
action "Channel"
action quiet "Clear 3"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 58"
action quiet "‹ Previous"
action quiet "Next ›"
popover "Delivery"
field "Garcia Interiors restock (41)" checkbox
field "Becker spring refill (12)" checkbox
field "Lindqvist studio courier (5)" checkbox
field "Counter sale (0)" checkbox
action quiet "Clear this filter"
Rationale Each filter option shows how many rows it matches under the other filters as set. The reader sees a zero before choosing it, rather than landing on an empty table. Fewer dead-end filterings instead of a better empty state afterwards.
Cost Each count is a read against the other filters, so opening one select costs several queries. Counts also go stale the moment another user acts, and a stale zero is worse than none.
40-pixels-the-default40 pixels, the defaultDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Linear states each condition as a chip with its field, its operator, and its value, and the chip removes itself. A compound filter reads as a sentence, and clearing one condition never touches the others.
Cost Chips wrap to several rows on a narrow screen. Building a condition needs a field menu, an operator menu, and a value menu in sequence.
Reference Linear
Row density#
patternguidanceQuestion How tall is a row in an orders table. Carbon defines five heights from 24 to 64 pixels, and the header, toolbar, and pagination match the row size. The default table runs near 47 pixels per row, between md and lg.
Default 40-pixels-the-default
Compact Below 640 pixels the table becomes two-line record rows with the amount right. Sorting and comparison down a column move to the desktop. The compact option is a-record-list-instead-of-a-table.
Rationale Keep 40 pixels. Two lines fit, the row scans, and it matches the default size. The runner-up is 32 pixels, for a working queue where every row is read once and acted on.
Never ship a density control the reader sets. Most readers set it once, and a screenshot suite then holds three versions of every page. The phone answer is a record list.
24-pixels-scannable24 pixels, scannablepage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide. Sorted by how long you have left."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Refunds"
table 7 "Order ↑" "Customer" "State" "Amount" "Time left"
item: text "3H3C68" · text "Elin Lindqvist" · status "Window closed" critical · text "EUR 50.00" · text "Closed 34 days ago"
footer
text "1 – 25 of 31"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Carbon md default. Two lines of text fit and the row scans.
Cost Twenty-five rows is 1000 pixels, so a 268-row table is eleven screens. Fine for browsing, slow for counting.
Reference Carbon
32-pixels-a-working-table32 pixels, a working tablepage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
toolbar
field "Search deliveries" search
tabs "Scheduled 3" "Past 1" "All 4"
region "Scheduled"
table 4 "Delivery ↑" "Date" "State" "Loaded" "Capacity"
item: text "Becker spring refill" · text "9 Oct 2026" · status "Scheduled" info · text "32" · text "40 pallets" · action "Open"
footer
text "1 – 4 of 4"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Carbon xs. One line per row, so the eye can travel straight down a column. The right choice for a queue a back-office user works through rather than reads.
Cost One line means the delivery name and the customer email have nowhere to go. Both have to be dropped or opened from the row.
Reference Carbon
a-density-control-the-reader-setsA density control the reader setspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "Rows" select = "Medium"
region "Orders"
table 8 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Carbon sm. Enough for a badge and one line, which covers a working table where every row gets an action.
Cost A two-line cell such as order number over delivery name does not fit, so the delivery has to become a column or drop.
Reference Carbon
dense-rows-with-a-zebra-passDense rows with a zebra passpage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Refunds"
table 6 "Order" "Customer" "State" "Amount" "Time left"
item: text "3H3C68" · text "Elin Lindqvist" · status "Window closed" critical · text "EUR 50.00" · text "Closed 34 days ago"
footer
text "1 – 25 of 31"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Three densities offered as a small segmented control in the toolbar, persisted per user. Carbon treats density as a token, not a free choice, so three steps is the honest number.
Cost A third toolbar control for something most readers set once and never think about. And a screenshot test then has to hold three versions of every page.
Reference Carbon
a-record-list-instead-of-a-tableA record list instead of a tablepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "State" select = "Any state"
region "Orders"
list 5
item: text "7K4Q2M · Garcia Interiors restock" · text "EUR 125.00" · text muted "Maria Garcia · maria@garcia-interiors.example" · status "Paid" positive · action "Open"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale At 24 pixels the eye loses its place across a wide row, so the Carbon zebra modifier returns. Every second row takes the muted surface.
Cost Zebra on a two-line row is noisy, because the bands cut through the cell content rather than sitting behind it. It works at one line and not at two.
Reference Carbon
comfortable-48-px-rows-for-a-delivery-listComfortable 48 px rows for a delivery listpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
toolbar
field "Search deliveries" search
tabs "Scheduled 3" "Past 1" "All 4"
region "Scheduled"
table 4 "Delivery" "State" "Loaded"
item: text "Becker spring refill" · text muted "Fri 9 Oct 2026 · Rotterdam depot" · status "Scheduled" info · text "32 of 40 pallets" · action "Open"
footer
text "1 – 4 of 4"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale One card-width block per record with two lines and its own commands on the right. It is what the phone layout wants.
Cost No columns, so no sorting and no comparison across a column. It trades every table capability for reading one record well.
compact-rows-with-a-header-that-staysCompact rows with a header that stayspage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide. Sorted by how long you have left."
text "The header stays while the queue scrolls."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Refunds"
table 8 "Order" "Customer" "State" "Amount" "Time left"
item: text "3H3C68" · text "Elin Lindqvist" · status "Window closed" critical · text "EUR 50.00" · text "Closed 34 days ago"
footer
text "1 – 25 of 31"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Shopify Polaris draws its product lists at a comfortable height with a second line under the name. Deliveries are read one by one rather than scanned down a column. The load bar answers the only question the row exists for.
Cost Eight rows need a scroll where the compact table shows fourteen. Wrong for any queue a back-office user works through in order.
Reference Shopify Polaris
a-bar-above-the-table-naming-the-countA bar above the table naming the countDefaultpage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Selected"
text "3 selected"
text muted "EUR 200.00 of order money"
action "Refund in full"
action "Refund part"
action primary "Refund these 3"
action quiet "Clear"
region "Refunds"
table 5 "Order" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale Linear lists keep the header pinned while hundreds of compact rows scroll under it, so the column names survive the scroll. The right shape for a queue that is worked top to bottom without paging.
Cost A pinned header only pays off on a long list. On five rows it is chrome with no job. It also needs a fixed list height, which fights the page scroll.
Reference Linear
Bulk action bar#
patternguidanceQuestion What appears once order rows are selected. The bar names the count and the money, then offers the commands. A refund on the wrong scope is the expensive mistake this page invites.
Default a-bar-above-the-table-naming-the-count
Compact The recommended option reflows below md. One column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep a bar above the table naming the count and the money. On a refunds page the total is the thing a reader checks before acting. The runner-up is a floating pill, for reversible bulk work such as resending confirmations or tagging, where the bar leaves the table in place.
Never ship the bar at the foot. On a long table the action sits off screen while the checkboxes that enabled it do not.
the-bar-replaces-the-toolbarThe bar replaces the toolbarpage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
region "Selected"
text "3 selected"
text muted "EUR 200.00"
action quiet "Clear"
action "Refund part"
action primary "Refund these 3"
region "Refunds"
table 5 "Order" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale The bar states how many are selected and what that total is worth, then offers the commands. The count is the thing a reader checks before acting on money.
Cost The toolbar is still visible above the bar, so two rows of controls stack. Either the toolbar goes or the page gets taller.
bar-at-the-foot-above-the-paginationBar at the foot, above the paginationpage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Refunds"
table 5 "Order" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
footer
text "2 selected · EUR 175.00"
action quiet "Clear"
action "Refund part"
action primary "Refund these 2"
footer
text "1 – 25 of 31"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale While a selection exists the toolbar is replaced by the bulk bar. One set of controls at a time, and the page cannot be read as offering search while it is offering actions.
Cost The layout shifts under the reader mid-decision. The search box they wanted to refine with is gone exactly when they are about to act.
bar-with-the-escalation-and-money-namedBar with the escalation, and money namedpage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
message "All 25 on this page are selected. 1,204 refunds match this filter in total." info
region "Selected"
text "25 selected"
text muted "EUR 1,125.00 of order money on this page"
action quiet "Clear"
action "Refund part"
action primary "Refund these 25"
region "Refunds"
table 5 "Order" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale The bar sits where the reader's eye already goes after ticking boxes, above the row count. The selection controls stay next to the count they describe.
Cost On a long table the foot is off screen while the checkboxes are not. The reader scrolls to find the action its checkboxes enabled.
bar-with-a-confirm-before-it-commitsBar with a confirm before it commitspage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Selected"
text "3 selected"
text muted "EUR 200.00"
action quiet "Clear"
action "Refund part"
action primary "Refund these 3"
region "Refunds"
table 5 "Order" "Customer" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
dialog "Refund 3 orders in full?"
text "EUR 200.00 goes back to 3 customers."
fact "Maria Garcia · 7K4Q2M" "EUR 125.00"
fact "Tom Becker · STT855" "EUR 50.00"
fact "Maria Garcia · PP0K9B" "EUR 25.00"
message "One of these orders is already 214 days old. The bank will not take that one back." caution
action "Cancel"
action destructive "Refund 3 orders"
Rationale The bar carries both numbers: 25 on this page, 1,204 matching. It also names the money, because a refund bulk action on the wrong scope is the expensive mistake this page invites.
Cost A long bar with two numbers and three buttons. It only fits above about 900 pixels, so the phone needs its own version of the same warning.
bar-with-the-action-inline-and-an-undo-afterBar with the action inline and an undo afterpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "State" select = "Any state"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
toast positive "Confirmations resent to 3 customers."
Rationale The primary button opens a dialog naming the count and the money. A bulk refund is exactly the action NN/g says to interrupt when it is rare and irreversible.
Cost Two steps for a batch the reader has spent a minute selecting. Acceptable on money, wrong for anything reversible such as a bulk tag.
Reference NN/g
a-floating-pill-with-the-count-and-one-menuA floating pill with the count and one menupage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
field "State" select = "Any state"
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid"
item: field "Select 7K4Q2M" checkbox · text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
popover "Selected"
text "3 selected"
action "Resend confirmation"
action "Add tag"
action quiet "More actions"
Rationale The bulk action runs with no confirmation and the bar becomes an undo toast. Carbon keeps an action-bearing toast until it is dismissed rather than letting a timer take the undo away.
Cost On money the evidence argues against this option. NN/g reports reflex clicking when confirmations are routine. A refund is not the place to train that reflex.
count-left-numbered-pages-rightCount left, numbered pages rightDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "1"
action quiet "2"
action quiet "3"
action quiet "4"
action quiet "5"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Stripe dashboard keeps the bulk bar to a count and one Actions menu, drawn open here. Every command is named in one place, the bar stays narrow, and adding a fifth command costs no width.
Cost Two clicks to the common command instead of one, and nothing on the bar says that resending is what most back-office users reach for.
Reference Stripe
Pagination#
patternguidanceQuestion How does a reader move through 268 orders. The range and two chevrons move one page at a time. Numbered pages add a jump, and the count still says where the reader is.
Default count-left-numbered-pages-right
Compact Below 640 pixels the numbered pages leave and the pager keeps the range with two chevrons. The compact option is a-compact-pager-range-plus-two-chevrons.
Rationale Keep the count with numbered pages. The reader can jump, and the range still says where they are. The runner-up is the compact pager, where width is tight and nobody jumps further than one page at a time.
Never ship the jump field. Typing a page number then Enter is slower than clicking for almost every reader.
count-page-size-and-chevronsCount, page size and chevronspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
field "Page size" select = "25 per page"
text "Page 1 of 11"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale The default shape with numbered pages added and the redundant Page 1 of 11 removed. The reader can jump, and the count still says where they are.
Cost Seven page numbers plus two chevrons is a wide control, and it has to collapse on a phone. The count and the numbers still overlap slightly.
first-previous-numbers-next-lastFirst, previous, numbers, next, lastpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "101 – 125 of 268"
action quiet "First"
action quiet "Previous"
action quiet "4"
action quiet "5"
action quiet "6"
action quiet "7"
action quiet "8"
action quiet "Next"
action quiet "Last"
Rationale Carbon's three parts: items per page, the current range and total, and the current page. The page size is the control most tables leave out and back-office users most want on a long list.
Cost Three controls in one bar. On a phone it becomes a row of three stacked lines, which pushes the table off screen.
Reference Carbon
count-chevrons-and-jump-to-a-pageCount, chevrons, and jump to a pagepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
field "Page" number = "1"
text "of 11"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Five controls plus a window of numbers. A reader on page 7 of 11 can reach either end in one click without scrolling the number strip.
Cost The widest pagination control there is, and the numbers still have to window or overflow. On a 268-row list it saves at most four clicks.
load-more-as-a-button-at-the-footLoad more as a button at the footpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
action "Load 25 more"
text muted "25 of 268 shown"
Rationale Two chevrons and a small jump field. Fewest controls, and a reader who knows the page number types it rather than clicking eight times.
Cost The jump field is a fourth thing to learn, and typing 11 then Enter is slower than clicking for most readers.
a-compact-pager-range-plus-two-chevronsA compact pager: range plus two chevronspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Search orders" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
footer
text "1 – 25 of 268"
action quiet "‹ Previous"
action quiet "Next ›"
Rationale Under about fifty records the whole list fits and pagination only adds a control that cannot do anything useful.
Cost It cannot be a rule for the whole back office. Orders reach thousands and deliveries reach hundreds, so the list page has to decide per query.
no-pager-at-all-while-everything-fitsNo pager at all while everything fitspage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "10 open orders, all shown below."
toolbar
field "Search refunds" search
region "Refunds"
table 8 "Order" "Customer" "Asked" "State" "Paid"
item: text "3H3C68" · text "Elin Lindqvist" · text "7 Oct 22:14" · status "Open" neutral · text "EUR 50.00"
Rationale Stripe dashboard pager is the range and two chevrons, nothing else. It fits a phone without collapsing, and on a 268-row list most readers move one page at a time anyway.
Cost No jumping: reaching page 9 costs eight clicks. Only right where the reader rarely leaves the first pages.
Reference Stripe
one-inline-action-the-rest-in-a-row-menuOne inline action, the rest in a row menuDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
action "Export CSV"
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00" · action "Resend" · menu "More"
Rationale GitHub draws no pagination on a list that fits one page: no range, no chevrons, no control that cannot do anything. The table ends and the page ends with it.
Cost Nothing states the total, so a reader cannot tell 10 rows from a filter that hid the rest. Only right where the heading already names the count.
Reference GitHub
Row actions#
patternguidanceQuestion An orders table shows rows of orders. Each row may carry its own commands. How many commands sit on the row, and where do the rest live.
Default one-inline-action-the-rest-in-a-row-menu
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep one inline command with the rest in the row menu. The common job stays one click away and the row stays narrow. Prefer the side preview where acting needs the record context, such as the lines before a resend. Never ship hover-revealed actions. Hover fails on touch, and a second focusable render doubles the table for no gain.
two-inline-actions-no-menuTwo inline actions, no menupage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide. Each row carries the two commands that apply to it."
toolbar
field "Search refunds" search
field "Window" select = "Any window"
region "Refunds"
table 5 "Order" "Customer" "State" "Amount" "Time left"
item: text "7K4Q2M" · text "Maria Garcia" · status "Open" neutral · text "EUR 125.00" · text "174 days left" · action "Refund" · action "Offer credit"
Rationale No commands on the row at all: clicking the order number opens the record, and every command lives there. The table is for finding, the record is for acting.
Cost A bulk job becomes one row at a time, so this shape fits finding, not acting.
one-destructive-command-in-the-row-guardedOne destructive command in the row, guardedpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
toolbar
field "Search deliveries" search
region "Deliveries"
table 4 "Delivery" "Date" "State" "Loaded"
item: text "Becker spring refill" · text "9 Oct 2026" · status "Scheduled" info · text "32 of 40 pallets" · action "Open" · menu "More"
popover
action "Open the delivery"
action "Duplicate the delivery"
action destructive "Discontinue"
Rationale The row action appears on hover, so a quiet table stays quiet. It stays in the tab order, so a keyboard reader reaches it. This is the accessibility repair on the hover-only pattern.
Cost It still fails on touch, where there is no hover, and the two fixes together mean two renders of every row. Polaris and NN/g both warn about hover-only affordances.
the-row-menu-drawn-openThe row menu drawn openpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00" · action "Resend" · menu "More"
popover
action "Open the order"
action "Resend confirmation"
action "Copy the order link"
action destructive "Void the order"
Rationale The row's second command is a removal. It is drawn in the destructive tone and marked as the last item in the row menu. It never sits inline next to a harmless one.
Cost A red word in every row is visual noise on a page for scanning states. Carbon warns that a tone with no action required stays plain text.
no-data-yet-table-replacedNo data yet, table replacedDefaultpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Six deliveries a week, from a 40-pallet restock to a single parcel."
action primary "New delivery"
region "Deliveries"
message positive "No deliveries yet. A delivery holds the date, the warehouse, the load and what is on it."
action primary "New delivery"
Rationale Shopify and Stripe open a row in a side panel that carries the record's facts and its commands together. Resending Maria Garcia's confirmation happens beside the order lines, so the reader checks what is sent before sending it.
Cost A panel over the list hides the rows it came from. On a phone it is a full screen, which makes it a page by another name.
Reference Shopify
Table's own states#
patternguidanceQuestion An orders table is sometimes empty, still loading, or failed to load. Each case needs its own words and layout. What shows for each state of the table itself.
Default no-data-yet-table-replaced
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep three sentences for three facts. No data yet, no results, and loading each replace the table they stand for. Prefer the slow-read note that names what is loading on nights when the list takes seconds. Never ship a bare spinner over the table body. Rows that shift when the data lands move the row the reader already chose.
no-results-for-a-search-filters-namedNo results for a search, filters namedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "No orders match this search."
toolbar
field "Order number, name or email" search = "maria becker"
field "Delivery" select = "Garcia Interiors restock"
field "State" select = "Paid"
region "Orders"
message neutral "Nothing matches maria becker. 268 orders exist, but none match this term with Garcia Interiors restock and Paid applied."
action "Clear the search"
action "Clear the filters too"
Rationale Nothing has ever existed, so the table and its headers are gone. One positive sentence offers the command that creates the first record.
Cost The reader cannot see what a row will look like, so the columns are undiscoverable until there is data.
loading-as-skeletons-at-the-real-row-heightLoading as skeletons at the real row heightpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid"
item: text muted "Order" · text muted "Customer" · text muted "Placed" · text muted "State" · text muted "Paid"
Rationale Something exists but nothing matches, so the copy names the term and the filters and offers to clear them. A different sentence from the no-data case, because a different thing happened.
Cost Two empty states means two pieces of copy per list page, both of which have to stay true as filters change.
a-failed-read-keeps-the-chromeA failed read keeps the chromepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
message critical "These orders could not be refreshed. What you see is from 2 minutes ago."
action "Try again"
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale Skeleton rows shaped like real rows at the real height, so nothing shifts when the data lands. Carbon asks for skeletons rather than a spinner over the body.
Cost Carbon also requires the header, the toolbar, and the pagination to match the row size. A skeleton page has to get all four heights right or it jitters.
Reference Carbon
a-failed-first-read-drops-the-tableA failed first read drops the tablepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
message critical "Orders could not be loaded. Nothing has been lost."
action primary "Try again"
Rationale The table rendered before, so a refresh failed. Keeping the headers and showing the error in place tells the reader the data they can see is stale rather than current.
Cost Old rows under an error can be acted on by mistake, which is the opposite of what the message wants to prevent.
refreshing-in-place-with-the-last-read-statedRefreshing in place, with the last read statedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
text "Read 12 s ago"
action quiet "Stop"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale Nothing ever loaded, so there is no table to keep. The empty state replaces the element it stands for, headers and footer included.
Cost The reader loses the column names, so a retry that succeeds looks like a different page rather than the page they were on.
Reference Carbon
no-results-with-recent-searchesNo results with recent searchespage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "No orders match this search."
toolbar
field "Order number, name or email" search = "maria becker"
field "Delivery" select = "Garcia Interiors restock"
region "Orders"
message neutral "Nothing matches maria becker. 268 orders exist, but none match this term."
action "Clear the search"
region "Recent searches"
list 3
item: link "maria garcia"
Rationale A queue reads itself on a timer. A quiet control says how often and when it last did, and offers to stop it.
Cost A timer on a page holding money means the reader may act on a value that changes a second later. A statement of when it last read is the mitigation, not a fix.
a-slow-read-that-says-what-it-is-doingA slow read that says what it is doingpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
action quiet "Stop"
region "Orders"
message info "Reading 268 orders. 6 s so far."
table 4 "Order" "Customer" "State" "Paid"
item: text muted "Order" · text muted "Customer" · text muted "State" · text muted "Paid"
Rationale The recent searches sit under a no-result state, so a mistyped term costs one click to repair instead of retyping. The recent terms are the reader's own, which makes them the most likely next query.
Cost Recent searches are stored state with their own lifecycle and their own privacy question. They also go stale as deliveries complete.
Reference Linear
a-badge-for-every-stateA badge for every statepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale A slow read is named while it runs: what is loading, how long it has taken, and a way to stop waiting. A reader who knows the list holds 268 orders waits. A reader watching a blank box leaves.
Cost A progress note needs the total before the rows arrive, which is a second read. It also teaches the reader that some lists are slow, which invites the question why.
Reference Vercel
Status in a column#
patternguidanceQuestion Each order row carries a state such as Paid or Disputed. Some states need a decision and some need none. How does the State column tell them apart.
Default tone-only-on-the-states-that-block
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Give a tone only to the states that block. Disputed and Awaiting payment take a badge because somebody must act. Paid and Completed stay plain words. Prefer the reason on a second line for a disputes queue where the word alone never answers why. Never ship progress marks. Four blocks per row on a 268-row list cannot serve a lifecycle that branches.
plain-text-and-the-label-says-which-statePlain text, and the label says which statepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
text muted "The State column holds one plain word."
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · text "Paid" · text "EUR 125.00"
Rationale Every state is a badge mapping a code to a word and a tone. The word is the signal. The tone repeats it.
Cost Carbon warns that more than five or six status indicators overwhelm the eye. A status needing no action stays plain text. A badge on all twenty-five rows is exactly that.
tone-only-on-the-states-that-blockTone only on the states that blockDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
text muted "Disputed and Awaiting payment are toned. Paid is not."
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "8M3DWR" · text "Ade Okafor" · text "8 Oct 06:22" · status "Awaiting payment" caution · text "EUR 45.00"
Rationale Plain words with no tone, because none of these states needs a decision. The header says State and the column holds one word, which keeps the interface quiet.
Cost A reader scanning for a problem has to read every word. With 268 rows that is slow, which is the case for badges.
two-states-side-by-side-both-namedTwo states side by side, both namedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
field "Payment" select = "Any payment"
field "Order state" select = "Any order"
region "Orders"
table 6 "Order" "Customer" "Placed" "Payment" "Order state" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · text "Paid" · text "Open" · text "EUR 125.00"
Rationale States that need a decision get a tone. The rest are plain words. This satisfies the plain-text rule for states needing no action and keeps the table readable. It is what the default orders table draws.
Cost The reader has to learn which states are toned, and a new state added later has to be classified before it is used.
a-dot-beside-the-wordA dot beside the wordpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · icon "dot" · text "Awaiting payment" · text "EUR 45.00"
Rationale This fixes the default table's actual problem rather than hiding it. Payment state and order state are separate columns. Paid and Completed stop looking like two names for one thing.
Cost A seventh column on a table that already carries five, for a distinction most readers do not need until something is wrong.
the-state-as-a-progress-of-three-stepsThe state as a progress of three stepspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
text muted "Placed, paid, packed, shipped."
table 4 "Order" "Customer" "Progress" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · image "Order progress, 3 of 4 steps done" · text "EUR 125.00"
Rationale The word carries the meaning and a small dot carries the tone. The column stays quiet and a scan still finds the one row that needs a decision.
Cost A dot beside a word is a third channel after word and badge. It adds an element that has to be explained once. It is also below 3:1 against the row background unless the token is chosen for it.
status-with-the-reason-on-a-second-lineStatus with the reason on a second linepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
field "Scope" select = "Needs attention"
region "Orders"
table 4 "Order" "Customer" "State" "Paid"
item: text "YB281K" · text "Ade Okafor" · text "7 Oct 19:02" · status "Disputed" critical · text muted "The bank asked for evidence" · text "EUR 45.00"
Rationale Where an order moves through a sequence, the column carries the sequence rather than the current step: placed, paid, packed, shipped. The reader sees both where it is and what is left.
Cost Four marks per row is heavy on a 268-row list, and it only works because the order lifecycle genuinely is linear. A state with branches cannot use it.
a-stale-wait-takes-a-stronger-toneA stale wait takes a stronger tonepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Six orders still waiting on money."
toolbar
field "Order number, name or email" search
field "Scope" select = "Awaiting payment"
region "Orders"
text muted "The tone follows the age of the wait."
table 4 "Order" "Customer" "State" "Paid"
item: text "8M3DWR" · text "Ade Okafor" · status "Awaiting payment" caution · text muted "2 days old" · text "EUR 45.00"
Rationale The payments list pairs the state with its reason: Disputed with why the bank asked, Awaiting payment with how old the wait is. The word says what, the line says why, and the reader never opens a row to learn the reason.
Cost Two lines doubles the row height for every row, including the paid ones whose reason is nothing. Only right on a queue where most rows need a decision.
Reference Stripe
number-only-right-alignedNumber only, right-alignedDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale Zendesk and Intercom color a waiting state by its age. An awaiting payment that is two hours old is neutral. At two days it turns caution. The tone answers how urgent the wait is, rather than only that there is one.
Cost The tone now depends on the clock, so the same row reads differently in the morning and the evening. A support reply that says the yellow one goes stale within hours.
Reference Zendesk
Money column#
patternguidanceQuestion Each order row shows what the customer paid. The column header names the currency. What does the money cell say beyond the number.
Default number-only-right-aligned
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep the number only. The header names the currency and repeating EUR on every row is noise. Prefer the refunded part under the charged amount where refunds are the work rather than the exception. Never ship money as a share of the total. A derived percentage column answers a question no orders list is asked.
symbol-and-value-one-columnSymbol and value, one columnpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid (EUR)"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "€125.00"
Rationale The default shape. A column headed Paid is unambiguous about the currency, so repeating EUR on every row is noise.
Cost EUR 0.00 reads as an error rather than as a fact. Two of the eight default rows are zero, and neither says why.
a-zero-amount-says-whyA zero amount says whypage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
field "Scope" select = "Any state"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "3H3C68" · text "Elin Lindqvist" · text "7 Oct 22:14" · text "Refunded" · text muted "Nothing, refunded"
Rationale The symbol sits in the cell rather than the word EUR, which is shorter and still unambiguous in a Dutch-language back office. The column header names it as Paid, EUR.
Cost A symbol alone fails WCAG 2.2 SC 1.4.1 if it is the only distinction. A screen reader does not always read a symbol prefix. The currency belongs in the label as well.
two-money-columns-charged-and-refundedTwo money columns: charged and refundedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
field "Scope" select = "Any state"
region "Orders"
text muted "Refunded is the total that went back to the customer."
table 6 "Order" "Customer" "Placed" "State" "Charged" "Refunded"
item: text "3H3C68" · text "Elin Lindqvist" · text "7 Oct 22:14" · text "Refunded" · text "EUR 50.00" · text "EUR 50.00"
Rationale EUR 0.00 is a real state, not an error, so the cell names it: Fully refunded. A bare zero reads as missing data, and the word says it is a fact.
Cost A word in a numeric column breaks the alignment. It has to be short enough not to push the column wide.
money-as-a-share-of-the-totalMoney as a share of the totalpage
breadcrumb "Home" "Deliveries" "Garcia Interiors restock" "Orders"
header
heading "Garcia Interiors restock"
text "1,204 orders in this delivery."
toolbar
field "Search orders" search
field "Scope" select = "Any state"
region "Orders"
table 6 "Order" "Customer" "Placed" "State" "Paid" "Share"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00" · text "100.0%"
Rationale Where refunds are common, the net matters more than the gross. A second column turns a negative into a fact rather than something the reader has to reconstruct.
Cost Two money columns on a five-column table is a lot of numbers. Most rows are identical in both, so the reader scans two equal columns to learn nothing.
a-totals-footer-under-the-moneyA totals footer under the moneypage
breadcrumb "Home" "Deliveries" "Garcia Interiors restock" "Orders"
header
heading "Garcia Interiors restock"
text "1,204 orders in this delivery."
toolbar
field "Search orders" search
field "Scope" select = "Any state"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
fact "Total on this page" "EUR 270.00"
text muted "The total covers the 25 orders on this page, not the 1,204 in the filter."
Rationale Rather than a derived percentage, the table is grouped by day with a subtotal per group. The reader sees the shape of the money without a chart.
Cost Grouping removes the flat row order and makes the table a different component, so pagination and select-all both get harder to reason about.
one-export-button-that-downloads-csvOne Export button that downloads CSVDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order for the Garcia Interiors restock, newest first."
toolbar
field "Order number, name or email" search
action "Export CSV"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale Stripe invoice lines keep one money column. They put what went back on its second line: the charged amount, then what returned to the customer. One column stays scannable and the refund is still on the row.
Cost Two lines doubles the row height, and a part refund needs a third fact, the net, which has nowhere to go in this cell.
Reference Stripe
Exporting the list#
patternguidanceQuestion A reader takes the orders list elsewhere. Download serves one spreadsheet now. Print serves a paper sheet. A schedule serves a repeated question. Which export does the list offer.
Default one-export-button-that-downloads-csv
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Keep one Export button that downloads the filtered rows as CSV. The accountant job costs one click and no decisions. Prefer a background export past a few thousand rows where a download would time out. Never ship copy as the only export. Copying the visible page silently drops every row past the pager.
scheduled-email-exportScheduled email exportpage
breadcrumb "Home" "Orders" "Scheduled exports"
header
heading "Scheduled exports"
text "Acme Supply receives its orders by email, on a rhythm it sets once."
action "New schedule"
region "Monday figures"
text "One schedule per recipient, each with its own rhythm and scope."
field "Every" select = "Monday 07:00"
field "Send to" text = "hello@acme-supply.example"
field "Scope" select = "Garcia Interiors restock"
message info "The next export runs Monday at 07:00 and covers the orders placed since the last one."
Rationale The Export button opens a dialog where the reader picks the format and the columns before anything downloads. The reader states what leaves the back office rather than trimming a fixed file afterwards.
Cost Two steps instead of one. The dialog has to remember or reset its choices on the next visit. The single button never has to decide.
print-stylesheet-with-a-print-buttonPrint stylesheet with a Print buttonpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order for the Garcia Interiors restock, newest first."
toolbar
field "Order number, name or email" search
action "Print"
region "Orders"
message info "Printing hides the sidebar and the toolbar and keeps the order rows."
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale The team sets a rhythm once and finance receives the orders without opening the back office. A Monday 07:00 email answers the weekly question before anyone asks it.
Cost A schedule is invisible state. Nobody sees it on the orders page. A stale recipient keeps receiving figures until someone remembers the schedule exists.
copy-the-list-to-the-clipboardCopy the list to the clipboardpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order for the Garcia Interiors restock, newest first."
toolbar
field "Order number, name or email" search
action "Copy visible rows"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
toast positive "25 visible orders copied as tab-separated values."
Rationale Each row carries its own receipt. Answering one customer means downloading one file rather than exporting the whole list and cutting it down. Order 7K4Q2M for Maria Garcia downloads as its own EUR 125.00 receipt.
Cost A button on every row narrows the table. It invites downloading receipts one by one for a job that wants the whole list. That is slower than one export.
a-large-export-that-runs-in-the-backgroundA large export that runs in the backgroundpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order for the Garcia Interiors restock, newest first."
toolbar
field "Order number, name or email" search
action disabled "Exporting"
region "Orders"
message info "Preparing 9,412 orders as CSV. The file arrives by email to hello@acme-supply.example."
text muted "34 percent done."
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale One button copies the visible rows as tab-separated values, which paste straight into a spreadsheet. Nothing downloads, so there is no file to find, rename, or delete afterwards.
Cost Only the visible page is copied. Pasting 25 rows when the filter holds 268 silently drops the rest unless the reader notices the pager.
export-history-with-repeat-downloadExport history with repeat downloadpage
breadcrumb "Home" "Orders" "Export history"
header
heading "Export history"
text "Every file that left the orders list, newest first."
action "New export"
region "Export history"
text "Files stay for 30 days, then they are removed."
table 4 "File" "Rows" "Asked" "By"
item: text "garcia-restock-orders-oct.csv" · text "268" · text "8 Oct 09:41" · text "Priya Shah" · action "Download"
Rationale The bulk export prepares a large file while the reader keeps working, then email sends a download link. Past a few thousand rows a direct download times out, and a progress note beats a failed one.
Cost The file arrives later, so the reader cannot reconcile against it right away. The export also needs its own failure state when the job dies overnight.
Reference Shopify
matches-highlighted-in-the-cellsMatches highlighted in the cellsDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "2 of 268 orders match maria."
toolbar
field "Order number, name or email" search = "maria"
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · text "Paid" · text "EUR 125.00"
Rationale The dashboard keeps every export in a list with its rows, its date, and who asked for it, and each one downloads again. The Monday figures are re-downloaded rather than re-built, and finance can see what left the back office.
Cost Stored files with customer data need their own retention and access rule. A history nobody prunes becomes a second database of personal data.
Reference Stripe
Searching with highlighted matches#
patternguidanceQuestion A search box filters the orders table. A match on an email looks the same as a match on a name. How does the list show why each row matched.
Default matches-highlighted-in-the-cells
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale Highlight the matches in the cells. The reader sees why each row matched without opening it. Prefer a scope control once readers search across deliveries and must say which one. Never ship a match column on its own. It spends a column stating what the highlight already shows.
scoped-search-this-delivery-or-all-deliveriesScoped search: this delivery or all deliveriespage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Searching Garcia Interiors restock for maria."
toolbar
field "Order number, name or email" search = "maria"
field "Scope" select = "Garcia Interiors restock"
region "Orders"
text "1 match in Garcia Interiors restock. 1 more in all deliveries."
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "Paid" · text "EUR 125.00"
Rationale The code search marks the matched text where it sits. The term stays visible in the box and every match glows in its cell, so the reader trusts the filter.
Cost Highlighting needs the match offsets from the server, not the rows alone. A client-side guess highlights the wrong occurrence on long text.
Reference GitHub
a-match-column-naming-where-it-matchedA match column naming where it matchedpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "3 of 268 orders match restock."
toolbar
field "Order number, name or email" search = "restock"
region "Orders"
table 4 "Order" "Customer" "Matched" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text muted "Delivery: Garcia Interiors restock" · text "EUR 125.00"
Rationale The search is scoped to the current view or the whole store from one control beside the box. A reader inside the Garcia Interiors restock searches it first and widens only when nothing matches.
Cost A scope is invisible once set. A reader who narrowed to one delivery last week searches it again today and wonders where the rest went.
Reference Shopify
first-match-focused-for-the-keyboardFirst match focused for the keyboardpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "2 of 268 orders match maria."
toolbar
field "Order number, name or email" search = "maria"
region "Orders"
text "Enter opens the first match. Up and down move between them."
table 4 "Order" "Customer" "State" "Paid"
item: link "7K4Q2M" · text "Maria Garcia" · text "Paid" · text "EUR 125.00"
Rationale The issue search says which field matched when the term sits somewhere the row does not show. The column answers why this row is here for matches in notes or metadata.
Cost A column spent stating what a highlight already shows, on every row, for every search. It only earns its width where rows match on hidden fields.
Reference GitHub
recent-searches-under-the-boxRecent searches under the boxpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order across every delivery, newest first."
toolbar
field "Order number, name or email" search
region "Orders"
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
popover
text "Recent searches"
action quiet "maria garcia"
action quiet "7K4Q2M"
action quiet "disputed"
action quiet "Clear recent searches"
Rationale The first result is focused as the reader types, so Enter opens it without a click. Support answering a caller moves from term to record without touching the mouse.
Cost A focused row looks selected, and Enter opening it surprises a reader who meant to refine the term. The hint has to teach the key or it misfires.
Reference Linear
a-searching-indicator-while-the-server-answersA searching indicator while the server answerspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Searching for maria garcia restock."
toolbar
field "Order number, name or email" search = "maria garcia restock"
action quiet "Stop"
region "Orders"
message info "Searching 268 orders for maria garcia restock."
Rationale The reader's own recent terms sit under an empty search box. A term searched twice a day costs one click instead of fourteen keystrokes.
Cost Recent terms are stored per user with their own privacy question, and a term from a finished delivery lingers until it is cleared.
Reference Vercel
grouped-by-day-with-subtotalsGrouped by day with subtotalsDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Newest first, grouped by day."
toolbar
field "Order number, name or email" search
field "Any state" select = "Any state"
region "Today, 8 Oct 2026, 4 orders"
fact "Subtotal" "EUR 270.00"
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
region "Yesterday, 7 Oct 2026, 2 orders"
fact "Subtotal" "EUR 95.00"
table 4 "Order" "Customer" "State" "Paid"
item: text "YB281K" · text "Ade Okafor" · status "Disputed" critical · text "EUR 45.00"
Rationale The dashboard says when a search is still running rather than showing yesterday's rows as if they matched. The reader waits instead of acting on a stale list.
Cost A visible search state needs debouncing and canceling, or every keystroke queues a request and the indicator flickers.
Reference Stripe
Grouped rows#
patternguidanceQuestion Some questions are asked per group, not per row: what did today take, what does each delivery hold. A flat table answers them only after an export, while a group header with its own subtotal answers them on the page.
Default grouped-by-day-with-subtotals
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The pick is (a), grouped by day with subtotals: it answers what today took without a chart and without leaving the list. The runner-up is (b), grouped by delivery with collapse toggles, once deliveries grow so one day of rows stops fitting. Never ship (f), two group levels: month-then-day nesting turns the list into a tree that paging cannot cross.
grouped-by-delivery-with-collapse-togglesGrouped by delivery with collapse togglespage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Grouped by delivery. Becker spring refill and Lindqvist studio courier are collapsed."
toolbar
field "Order number, name or email" search
field "Group" select = "Delivery"
region "Garcia Interiors restock, 3 orders"
fact "Subtotal" "EUR 225.00"
action quiet "Collapse"
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
region "Becker spring refill, 2 orders"
fact "Subtotal" "EUR 90.00"
action quiet "Expand"
region "Lindqvist studio courier, 1 order"
fact "Subtotal" "EUR 45.00"
action quiet "Expand"
Rationale Bank statements and Stripe payouts group money by day with a subtotal per group. The reader sees the shape of the takings and still opens any row.
Cost Grouping removes the flat row order, so pagination and select-all both get harder to reason about across group edges.
Reference Stripe
grouped-by-stateGrouped by statepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Grouped by state, disputed first."
toolbar
field "Order number, name or email" search
field "Group" select = "State"
region "Disputed, 1 order"
fact "Subtotal" "EUR 45.00"
table 4 "Order" "Customer" "Placed" "Paid"
item: text "YB281K" · text "Ade Okafor" · text "7 Oct 19:02" · text "EUR 45.00"
region "Awaiting payment, 1 order"
fact "Subtotal" "EUR 45.00"
table 4 "Order" "Customer" "Placed" "Paid"
item: text "8M3DWR" · text "Ade Okafor" · text "8 Oct 06:22" · text "EUR 45.00"
region "Paid, 3 orders"
fact "Subtotal" "EUR 220.00"
table 4 "Order" "Customer" "Placed" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · text "EUR 125.00"
Rationale Linear's group-by puts a toggle on each group header, so a reader following one delivery collapses the rest. The header carries the count and the subtotal even while collapsed.
Cost Collapsed groups hide rows from find-in-page and from a reader who forgot they collapsed anything. The toggle state needs to survive a reload or it annoys.
Reference Linear
no-grouping-a-group-column-that-sortsNo grouping: a group column that sortspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Sorted by delivery, newest first inside each delivery."
toolbar
field "Order number, name or email" search
field "Sort" select = "Delivery"
table 5 "Order" "Delivery" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Garcia Interiors restock" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale Linear groups issues by status so the blocked ones read as one block. Here the disputed and awaiting rows sit together at the top, which is the triage order support wants.
Cost States change under the reader: paying an order moves it to another group, which reads as the row vanishing. The list has to say where it went.
Reference Linear
group-headers-that-select-the-groupGroup headers that select the grouppage
breadcrumb "Home" "Orders" "Refunds"
header
heading "Refunds"
text "31 orders to decide, grouped by day."
toolbar
field "Search refunds" search
field "Any window" select = "Any window"
region "Today, 8 Oct 2026, 2 selected"
field "Select today, 8 Oct 2026" checkbox
fact "Subtotal" "EUR 175.00"
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
region "Yesterday, 7 Oct 2026, 2 orders"
field "Select yesterday, 7 Oct 2026" checkbox
fact "Subtotal" "EUR 95.00"
table 4 "Order" "Customer" "State" "Paid"
item: text "YB281K" · text "Ade Okafor" · status "Disputed" critical · text "EUR 45.00"
Rationale Carbon keeps the table flat and lets the reader sort by the would-be group field instead. Nothing hides, paging stays simple, and the eye groups adjacent equal values on its own.
Cost No subtotals: the reader who wants today's takings sorts by date and adds the rows up by hand. Sorting is not grouping, it only looks like it.
Reference Carbon
two-levels-month-then-dayTwo levels: month, then daypage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Grouped by month, then by day."
toolbar
field "Order number, name or email" search
field "Any state" select = "Any state"
region "October 2026, 231 orders"
fact "Subtotal" "EUR 10,395.00"
text "Today, 8 Oct 2026, 4 orders, EUR 270.00"
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
region "September 2026, 37 orders"
fact "Subtotal" "EUR 1,665.00"
Rationale Outlook and Gmail let a reader tick a whole conversation from its header. Here one checkbox refunds the day orders together, which is the batch the team runs.
Cost Selecting a group selects rows the reader may not have seen, including a disputed one that should never be batch-refunded. The confirm has to name them all.
Reference Outlook
expand-to-the-order-linesExpand to the order linesDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order, newest first."
toolbar
field "Order number, name or email" search
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
card "7K4Q2M lines"
action quiet "Collapse"
fact "Oak desk lamp, 2" "EUR 90.00"
fact "Desk chair, 1" "EUR 35.00"
Rationale Xero and bank statements nest day groups inside month groups for a year of rows. October holds its days, and the year reads as twelve subtotals before any row is shown.
Cost Two levels of headers for 268 rows is mostly chrome: the month header, the day header, then one row. Nesting pays off past thousands of rows, not hundreds.
Reference Xero
Inline row expansion#
patternguidanceQuestion Some rows hold a second level: an order holds its lines, its units, its history. Opening the record for each one costs a page load per row. An expansion row under the row itself answers the question where it was asked.
Default expand-to-the-order-lines
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The pick is (a), expansion to the order lines: it answers what the customer bought, which is the question asked from the list. The runner-up is (c), units with pickup state, on the pickup list where handover is the work. Never ship (e), expand-all: twenty expanded rows is a page nobody can scan, and the control exists to punish curiosity.
expand-to-the-order-timelineExpand to the order timelinepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order, newest first."
toolbar
field "Order number, name or email" search
table 4 "Order" "Customer" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
card "7K4Q2M history"
action quiet "Collapse"
timeline 3
item: text "Placed" · text muted "8 Oct 2026, 09:32"
Rationale Stripe's invoice list expands a row to its lines: what was bought, at what price, adding to the row total. Support answers 'what did I pay for' without leaving the list.
Cost The detail row breaks the column rhythm, and sorting reorders parents while children follow, which confuses a reader who sorted by money.
Reference Stripe
expand-to-units-and-pickup-stateExpand to units and pickup statepage
breadcrumb "Home" "Deliveries" "Garcia Interiors restock" "Pickup list"
header
heading "Pickup list"
text "Garcia Interiors restock, today. What is collected and what is not."
toolbar
field "Customer name or order number" search
table 3 "Order" "Customer" "Collected"
item: text "V7VZNX" · text "Elin Lindqvist" · text "1 of 2"
card "V7VZNX units"
action quiet "Collapse"
fact "Oak desk lamp A-1041, Counter 1" "09:41"
fact "Oak desk lamp A-1042" "Not yet"
Rationale Zendesk expands a record to its history: placed, paid, confirmation sent. The history sits under the row it belongs to, newest last, so the reader sees what happened in order.
Cost History grows without bound: a disputed order's expansion is a page of its own. It needs trimming to the last few entries with a link to the rest.
Reference Zendesk
one-row-open-at-a-timeOne row open at a timepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Opening a row closes the one that is open."
toolbar
field "Order number, name or email" search
table 4 "Order" "Customer" "State" "Paid"
item: text "STT855" · text "Tom Becker" · status "Paid" positive · text "EUR 50.00"
card "STT855 lines"
action quiet "Collapse"
fact "Oak desk lamp, 1" "EUR 45.00"
fact "Shipping, 1" "EUR 5.00"
Rationale A pickup list expands an order to its units with what is collected and where. The warehouse lead works the list at the counter instead of opening forty records.
Cost Pickup state changes by the second at busy times, so an expanded row goes stale while it is open. It needs the same refresh the pickup screen has.
an-expand-all-control-above-the-tableAn expand-all control above the tablepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order, newest first."
toolbar
field "Order number, name or email" search
action "Collapse all"
table 3 "Order" "Customer" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "EUR 125.00"
card "7K4Q2M lines"
fact "2 Oak desk lamps" "EUR 90.00"
fact "Desk chair, 1" "EUR 35.00"
card "STT855 lines"
fact "Oak desk lamp, 1" "EUR 45.00"
fact "Shipping, 1" "EUR 5.00"
Rationale Carbon's accordion rule applied to the table: opening a row closes the last one. The list never grows past one detail, so the reader cannot lose their place in it.
Cost Comparing two orders means opening, memorizing, closing, and opening. The rule trades comparison for tidiness.
Reference Carbon
no-expansion-the-row-opens-the-recordNo expansion: the row opens the recordpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "The order number opens the record. Nothing expands in place."
toolbar
field "Order number, name or email" search
table 4 "Order" "Customer" "State" "Paid"
item: link "7K4Q2M" · text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale GOV.UK's accordion pairs every toggle with a Show all control, so a reader checking every order's lines presses once instead of twenty-five times.
Cost Twenty expanded rows is a page nobody can scan, and each detail needs its own data, so Show all fires twenty-five reads at once.
Reference GOV.UK
the-order-column-pinned-while-the-rest-scrollThe order column pinned while the rest scrollDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "The order column stays while the rest scroll under it."
toolbar
field "Order number, name or email" search
table 8 "Order" "Delivery" "Placed" "Customer" "Channel" "State" "Method" "Paid"
item: text "7K4Q2M" · text "Garcia Interiors restock" · text "8 Oct 2026, 09:32" · text "Maria Garcia" · text "Online" · status "Paid" positive · text "Bank transfer" · text "EUR 125.00"
Rationale The order number is a link and everything lives on the record page. One pattern for detail everywhere, and the list stays a list.
Cost Every question costs a page load and a back button. Checking three orders means six page loads where expansion needs none.
Pinned and resizing columns#
patternguidanceQuestion A wide table scrolls, and a scrolling table loses its bearings. The order code leaves as the money arrives. Pinning the identity column keeps the row named, and resizing lets the reader spend width where their own data needs it.
Default the-order-column-pinned-while-the-rest-scroll
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The pick is (a), the order column pinned while the rest scroll: the row stays named at any scroll position, and it costs no control. The runner-up is (b), the actions pinned too, once rows carry inline commands that must stay reachable. Never ship (c), draggable widths, in a first release: per-user widths are stored state that support cannot see and screenshots cannot hold.
first-and-last-pinned-actions-always-visibleFirst and last pinned, actions always visiblepage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Order stays left, Resend stays right."
toolbar
field "Order number, name or email" search
table 7 "Order" "Delivery" "Customer" "Channel" "State" "Paid" "Action"
item: text "7K4Q2M" · text "Garcia Interiors restock" · text "Maria Garcia" · text "Online" · status "Paid" positive · text "EUR 125.00" · action "Resend"
Rationale Shopify's orders index pins the order column and lets the other seven scroll under it. Nothing is folded, so every fact keeps its own scannable column.
Cost On a phone the reader sees the order code and one other column at a time. Pinning keeps the bearings, not the overview.
Reference Shopify
draggable-widths-with-resize-handlesDraggable widths with resize handlespage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Drag a header edge to resize. Double-click it to fit the content."
toolbar
field "Order number, name or email" search
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 2026, 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale Airtable pins the record name on the left and the row commands on the right, so both survive any scroll position. Acting never requires scrolling back.
Cost Two pinned columns on a phone leave no room for the middle at all. The pattern needs a wide screen to earn its keep.
Reference Airtable
pin-any-column-from-its-header-menuPin any column from its header menupage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "The Customer column menu is open."
toolbar
field "Order number, name or email" search
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 2026, 09:32" · status "Paid" positive · text "EUR 125.00"
popover
action "Sort ascending"
action "Sort descending"
action "Pin to the left"
action "Hide column"
Rationale Airtable and Retool put a handle on every header edge: drag it and the column follows. A reader with long delivery names spends width there instead of on the order code.
Cost Widths are per-user stored state that support cannot see, screenshots cannot hold, and a new column resets. A wide table of eight resizable columns is also eight ways to break the layout.
Reference Airtable
double-click-a-header-edge-to-fit-contentDouble-click a header edge to fit contentpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "The customer column is fitted to 150 pixels. Double-click its header edge to fit the longest value instead."
toolbar
field "Order number, name or email" search
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia, maria@garcia-interiors.example" · text "8 Oct 2026, 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale Carbon's default: the table scrolls as one piece and every column leaves together. No sticky edges, no shadows, no special cases in the CSS.
Cost Halfway across, the money column arrives and the order code is gone. The reader scrolls back to check which row the amount belongs to.
Reference Carbon
delivery-cards-with-a-load-barDelivery cards with a load barDefaultpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
action primary "New delivery"
toolbar
field "Search deliveries" search
grid 4
card "Becker spring refill"
image "Route map"
text "Fri 9 Oct 2026, Rotterdam depot"
status "Scheduled" info
fact "Loaded" "32 of 40 pallets"
action "Open"
Rationale Google Sheets fits a column to its widest value on a double-click of the header edge. No dragging, no stored widths: the table answers the data it holds right now.
Cost Nobody discovers a double-click on a header edge without being told, and fitting to content reintroduces the page-2 shift that fixed widths prevent.
Reference Google Sheets
Deliveries as a grid#
patternguidanceQuestion Deliveries are few and dated: the company runs a handful a week and asks how each one is doing. A table answers with rows of numbers, while a card answers with the run itself: its date, its warehouse, and how full it is.
Default delivery-cards-with-a-load-bar
Compact The recommended option reflows: one column, header actions under the title, and a table scrolls inside its own region.
Rationale The pick is (a), delivery cards with a load bar. Four deliveries read better as four cards than as four rows. The bar answers the only question the list exists for. The runner-up is (c), a date-led list, once the year holds dozens of runs and cards stop fitting. Never ship (d), the month calendar: it answers when, which the deliveries list is never asked.
a-table-with-a-load-progress-columnA table with a load progress columnpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
action primary "New delivery"
toolbar
field "Search deliveries" search
table 5 "Delivery" "Date" "State" "Loaded" "Action"
item: text "Becker spring refill" · text "9 Oct 2026" · status "Scheduled" info · text "32 of 40 pallets" · action "Open"
Rationale Each delivery is a card with its date, its warehouse, and how full it is. Four deliveries a week read as four runs, not as four rows of a table.
Cost Cards stop scanning past a dozen deliveries: no column to sort, no state to scan down. The pattern fits a handful, not a hundred.
a-date-led-list-with-month-dividersA date-led list with month dividerspage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
action primary "New delivery"
toolbar
field "Search deliveries" search
region "October 2026"
list 2
item: text "Becker spring refill" · text muted "Rotterdam depot, 08:00" · status "Scheduled" info · text "32 of 40 pallets"
region "December 2026"
list 1
item: text "Okafor office setup" · text muted "Amsterdam warehouse, 08:00" · status "Draft" neutral · text "Not scheduled yet"
Rationale Shopify's product list keeps the table and adds an inventory bar per row. The deliveries stay sortable and comparable, and the bar still answers how full each one is.
Cost A bar in a table row is a chart without an axis. 80 percent of 40 pallets and 53 percent of 60 pallets read as two bars of different lengths for different totals.
Reference Shopify
a-month-calendar-with-delivery-chipsA month calendar with delivery chipspage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "October 2026. Two runs scheduled."
action primary "New delivery"
toolbar
action quiet "Previous month"
action quiet "Next month"
region "October 2026"
grid 31
item: text "9" · text "Becker spring refill"
Rationale Google Calendar's schedule view leads with the date and groups by month. A planner reads October, then December, then March, each run under its own day.
Cost Dates lead and states follow, so finding the one draft means reading every month. The view answers when, not what needs work.
Reference Google Calendar
compact-rows-with-a-status-wordCompact rows with a status wordpage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
action primary "New delivery"
toolbar
field "Search deliveries" search
table 4 "Delivery" "Owner" "State" "Date"
item: text "Becker spring refill" · text "Alex Morgan" · text muted "Scheduled" · text "9 Oct 2026"
Rationale Google Calendar draws the month as a grid and each delivery as a chip on its day. Clashes show as two chips on one square, which no list makes visible.
Cost Four deliveries on thirty squares is mostly empty grid, and nothing on a chip says how full the run is. The calendar answers scheduling, not loading.
Reference Google Calendar
a-table-grid-toggle-the-reader-setsA table/grid toggle the reader setspage
breadcrumb "Home" "Deliveries"
header
heading "Deliveries"
text "Four deliveries, three with a date still ahead."
action primary "New delivery"
toolbar
field "Search deliveries" search
field "View" select = "Grid"
grid 2
card "Becker spring refill"
image "Route map"
text "Fri 9 Oct 2026, Rotterdam depot"
status "Scheduled" info
fact "Loaded" "32 of 40 pallets"
action "Open"
Rationale Linear's project list is one quiet row per project: name, owner, status, date. A team holding forty runs scans states down one column instead of opening forty cards.
Cost Quiet rows hide the load: nothing says how full a run is until it is opened. The list answers state, not loading.
Reference Linear
two-line-rows-with-money-rightTwo-line rows with money rightDefaultpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every paid order, newest first."
toolbar
field "Order number, name or email" search
field "Any state" select = "Any state"
list 6
item: link "7K4Q2M" · text "EUR 125.00" · text muted "Maria Garcia, 8 Oct 09:32" · status "Paid" positive
Rationale Notion lets the reader switch a view between table and board without leaving the page. Finance keeps the table, the warehouse lead keeps the cards, and both read the same deliveries.
Cost Two views of one list is two layouts to build, two to test, and a toggle whose state must survive reloads and shared links.
Reference Notion
List on a phone#
patternguidanceQuestion At 390 pixels the five-column table cannot survive: the columns either crush or scroll. Every option here answers the same question, what the orders list becomes on a phone. Each one keeps a different capability: reading, sorting, acting, or comparing.
Default two-line-rows-with-money-right
Compact Below 640 pixels the table becomes two-line rows with the amount right. Sorting moves to a Sort control. The compact option is two-line-rows-with-money-right.
Rationale The pick is (a), two-line rows with money right. It reads well at 390 pixels and keeps the order code, the customer, and the amount. It pairs with a Sort control where sorting matters. The runner-up is (c), a scroll container with a sticky first column, for the rare phone task that needs every column. Never ship (f), search hiding the list: a list that starts empty teaches the reader there is nothing to see.
stacked-records-with-labeled-cellsStacked records with labeled cellspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Every cell keeps its label. 268 orders, two shown."
toolbar
field "Order number, name or email" search
region "7K4Q2M"
fact "Delivery" "Garcia Interiors restock"
fact "Customer" "Maria Garcia"
fact "Placed" "8 Oct 2026, 09:32"
fact "State" "Paid"
fact "Paid" "EUR 125.00"
region "STT855"
fact "Customer" "Tom Becker"
fact "Paid" "EUR 50.00"
Rationale Stripe's mobile list and Polaris condensed rows read as order over customer on the left, money over state on the right. Everything fits 390 pixels with nothing scrolling sideways.
Cost No headers, so no sorting and no comparison down a column. Sorting moves to a control, and comparing moves to the desktop.
Reference Stripe
a-scroll-container-with-a-sticky-first-columnA scroll container with a sticky first columnpage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Scroll sideways. The order column stays."
toolbar
field "Order number, name or email" search
table 5 "Order" "Customer" "Placed" "State" "Paid"
item: text "7K4Q2M" · text "Maria Garcia" · text "8 Oct 09:32" · status "Paid" positive · text "EUR 125.00"
Rationale GOV.UK's responsive tables stack each row into labeled values on a narrow screen. Nothing is dropped and nothing needs discovering: every fact keeps its name.
Cost One order fills the screen, so six orders is six screens of scrolling. Thorough, and too long to scan.
Reference GOV.UK
priority-columns-onlyPriority columns onlypage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Order, placed and email return on the record."
toolbar
field "Order number, name or email" search
table 3 "Customer" "State" "Paid"
item: text "Maria Garcia" · status "Paid" positive · text "EUR 125.00"
Rationale Shopify's mobile tables keep every column and scroll sideways under a pinned first column. Nothing is redesigned for the phone: the table is the table, with its bearings kept.
Cost Sideways scrolling hides four of five columns at any moment, and comparing two columns means scrolling between them. The phone shows a window, not the list.
Reference Shopify
a-card-per-order-with-its-actionsA card per order with its actionspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "268 orders, one card each."
toolbar
field "Order number, name or email" search
grid 4
card "7K4Q2M, EUR 125.00"
text "Maria Garcia, Garcia Interiors restock"
status "Paid" positive
action "Open"
action "Resend"
Rationale Financial tables keep customer, state, and money on a narrow screen and drop the rest. Three columns fit 390 pixels without scrolling, and opening the row recovers the dropped facts.
Cost The order code, the placed time, and the delivery all leave. Support identifying a caller by code finds the one column they need is gone.
search-and-chips-above-one-line-rowsSearch and chips above one-line rowspage
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Find an order, then open it."
toolbar
field "Search orders" search
row
action "Needs attention"
action "Paid"
action "This week"
list 6
item: text "Maria Garcia" · text "EUR 125.00" · text muted "7K4Q2M, Garcia Interiors restock" · status "Paid" positive
Rationale Mobile order apps draw each order as a card with its own Resend button. The phone is for acting on one order, not for comparing twenty, so each card carries its command.
Cost Cards are tall: three orders fill the screen where rows fit six. A queue worked top to bottom becomes twice the scrolling.
search-and-chips-above-one-line-rows: Search and chips above one-line rows#
page
breadcrumb "Home" "Orders"
header
heading "Orders"
text "Find an order, then open it."
toolbar
field "Search orders" search
row
action "Needs attention"
action "Paid"
action "This week"
list 6
item: text "Maria Garcia" · text "EUR 125.00" · text muted "7K4Q2M, Garcia Interiors restock" · status "Paid" positive
Rationale Gmail's mobile list leads with search and a chip row, then one line per message. Finding beats browsing on a phone, so the list spends its width on the query, not the columns.
Cost One line per order holds the customer or the code, never both with the money. The row identifies enough to be opened, nothing more.
Reference Gmail
Verification#
| ID | Mark | Evidence |
|---|---|---|
| litespec/frontend/patterns/lists-and-tables#keep-a-lists-view-in-its-address | guidance | Does every built screen of this kind keep this rule, including in its compact layout? |
| litespec/frontend/patterns/lists-and-tables#align-money-to-the-right-with-its-currency | guidance | Does every built screen of this kind keep this rule, including in its compact layout? |
| litespec/frontend/patterns/lists-and-tables#choosing-the-columns | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#sorting | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#filters | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#row-density | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#bulk-action-bar | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#pagination | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#row-actions | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#tables-own-states | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#status-in-a-column | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#money-column | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#exporting-the-list | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#searching-with-highlighted-matches | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#grouped-rows | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#inline-row-expansion | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#pinned-and-resizing-columns | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#deliveries-as-a-grid | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |
| litespec/frontend/patterns/lists-and-tables#list-on-a-phone | guidance | Does the built page follow the default option, or the option the project catalog records with its reason? |