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.
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”- ✅
<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 anaria-labelthat 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 - ❌
<Badge variant="success">Published</Badge>(a word in a badge; useTag) - ❌
<Tag variant="accent">3</Tag>(a count in a tag; useBadge) - ❌
<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 andemptyTextspeaks) - ❌
<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; useTableorDataTable)