Data display
Records, statuses, counts, and loading states each have one component. This page settles the pairs that get confused: Table against DataTable, Badge against Tag, the deprecated Label, and when a collection shows an EmptyState instead of an empty table. It also fixes how loading is shown.
- Layer
- Pattern
- Rules
- 15 numbered
- Examples
- 10 do and don’t
- Updated
- Source
- data-display.md
One component per job
| Story | Section | Status | Reads |
|---|---|---|---|
| Field notes from the coast | Travel | Published | 12,840 |
| The quiet return of letterpress | Culture | In review | 0 |
| A week without a phone | Essays | Blocked | 0 |
Tableis for static, hand-composed markup:Table.Container,Table.Title,Table.Subtitle,Table.Actions, thenTable.Head,Table.Body,Table.Row,Table.Header,Table.Cell. Use it when the layout is bespoke (grouped rows, custom toolbars, server-driven paging) or the data does not change.DataTableis for a list of records the user sorts, searches, or filters client-side. It takescolumnsanddata, and builds the toolbar, sort buttons, and filter dropdowns. If the code around aTablestarts holding sort or filter state, it should be aDataTable.- In a
DataTablecolumn,accessorreturns the raw value used for sorting and filtering;cellis display only. Sorting never runs on rendered output. - Numeric columns use
align="end"on bothTable.HeaderandTable.Cell. A sortableTable.HeadersetssortDirection(ascending,descending,none) and handlesonClick; the component renders a real button andaria-sort. - Paged data uses
Paginationbelow the table withpage,pageCount,onPageChange. Row density comes fromdensity(condensed,normal,spacious), never from cell padding. Badgeis a count or a dot attached to something else (unread count on a nav item, a presence dot on an avatar). It showscount(collapsing pastmax, default99+) ordot. ABadgenever contains a word.Tagis a word.TagwithoutonRemoveis a read-only status or category pill (<Tag variant="success">Published</Tag>);TagwithonRemoveis a removable chip (an active filter, a recipient).variantisdefault,accent,success,danger,attention, ormuted, chosen by meaning.Labelis deprecated since 0.4.0. New code usesTagfor status pills; existing<Label variant="…">maps one-to-one to<Tag variant="…">.- Status colours on a
Tagmean status:successfor published or passing,dangerfor failed or blocked,attentionfor needs review. Categories and teams usedefaultormuted. A status is always a word, never a bare coloured dot. EmptyStaterenders when a collection has zero records. Itstitlenames what is empty (“No stories yet”); itsdescriptionsays why or what to do next (“Stories you draft or import appear here.”);actionis at most oneButton variant="primary";secondaryActionis an optionalButton variant="invisible"orLink;iconis optional and decorative (nolabel).headingLevel(2,3,4) sets the title’s rank to fit the page outline;size="large"is for a whole-page or first-run state,medium(default) for an empty region inside a page.- An empty search or filter result does not replace the table. The table, its toolbar, and the active filters stay on screen, and the message goes through
DataTableemptyText(default “No matching rows.”): either a sentence (“No stories match these filters.”) or, when the user needs a way out, a compactEmptyStatewithheadingLevel={4}whose title echoes the query and whoseactionis “Clear filters”. A hand-composedTablerenders oneTable.Cellwith the same kind of content. A full-sizeEmptyStatein place of the table is only for zero records (rule 10). Avataralways receivesname; it produces the initials fallback and the accessible name.shape="circle"is for people,shape="rounded"for organisations and repositories. Sizes come fromsize, never from width and height overrides.- Loading a layout whose shape is known uses
Skeletonblocks matching the final content, inside a container witharia-busy="true"and a visually hidden status text.Skeletonisaria-hiddenon its own. - A short indeterminate wait with no shape to mimic uses
Spinnerwith a meaningfullabel(“Loading stories”). A measurable job usesProgressBarwithvalue,max, andaria-label; an unknown-duration job usesProgressBarwithoutvalue. - Skeletons and spinners are never shown at the same time for the same region, and neither replaces an
EmptyStateonce the data has loaded empty.
Which one
Section titled “Which one”| Need | Component |
|---|---|
| Rows that never re-sort in the browser | Table |
| Rows the user sorts, searches, or filters | DataTable |
| A number or a dot on another control | Badge |
| A status or category word | Tag (no onRemove) |
| A user-removable token | Tag with onRemove |
| A status pill in 0.3.0 code | Label, deprecated; migrate to Tag |
| Zero records in a collection | EmptyState |
| A filter that matches nothing | DataTable emptyText (a sentence, or a compact EmptyState headingLevel={4}) |
| Known layout still loading | Skeleton |
| Unknown wait, compact | Spinner |
| Measurable job | ProgressBar |
Do / Don’t
Section titled “Do / Don’t”Do
<DataTable columns={cols} data={stories} title="Stories" rowKey={(s) => s.id} />With { id: 'status', header: 'Status', accessor: (s) => s.status, filterable: true, cell: (s) => <Tag variant={s.status === 'published' ? 'success' : 'muted'}>{s.statusLabel}</Tag> }
<Badge variant="danger" count={unread} />Next to an aria-label that includes the count (“Inbox, 3 unread”)
stories.length === 0 ? <EmptyState icon={<InboxIcon size={24} />} title="No stories yet" description="Stories you draft or import appear here." action={<Button variant="primary" onClick={create}>Create story</Button>} /> : <DataTable … /><div aria-busy="true"><Skeleton variant="text" /><Skeleton variant="text" width="80%" /></div>While a card body loads
Don’t
<Badge variant="success">Published</Badge>A word in a badge; use Tag
<Tag variant="accent">3</Tag>A count in a tag; use Badge
<Label variant="danger">failing</Label>In new code (deprecated; <Tag variant="danger">Failing</Tag>)
filtered.length === 0 ? <EmptyState title="No results" /> : <DataTable … />Replacing the table and its filters when a search returns nothing; the table stays and emptyText speaks
<EmptyState title="Nothing here" action={<Button>Import</Button>} secondaryAction={<Button variant="primary">Create</Button>} />Title does not name what is empty; two primary-looking actions
A <div> of flex rows with hardcoded borders standing in for a table (no <table> semantics; use Table or DataTable)