Requirement
When the content is tabular — rows of records compared across the same columns — render a real table and follow the table pattern:
- Semantics: a
<table>with<th>column headers (A11Y-7), not a grid of<div>s. - Alignment by data type: numeric and quantitative columns (amounts, counts, dates, currency) right-aligned with tabular figures (TYP-5) so digits line up for scanning and comparison; text columns (names, categories) left-aligned; the header aligns with its column.
- Persistent header: for any table long enough to scroll, the header row stays visible (sticky header, or it repeats) so columns are never unlabelled.
- Designed empty and loading states (CMP-3): a table that can be empty says so ("No customers in this list yet"), and one that loads over the network shows a loading state, not a blank frame.
Density follows LAY-5 (an entry-dense data grid is legitimately tighter than a summary table); row separation uses spacing or hairline dividers, not nested-card chrome (SLP-4). Whether the data even belongs in a table — rather than cards or a list — is a pattern-fit judgment (SLP-11): a table earns its rows when records are compared across shared columns.
Rationale
These are number-dense, table-heavy products: ledgers, rosters, timesheets, score summaries. A table is a scanning instrument, and alignment is what makes it work — a person reading down a column of 40 figures compares them at a glance only when the digits line up and the header stays put. Centre-aligned numbers, proportional figures, or a header that scrolls away each cost a beat of re-reading on every row, which compounds across a full list. Faking a table with stacked cards or divs throws away the row/column relationship screen readers and keyboard users rely on (A11Y-7) and usually the alignment too. The table pattern is Utility by Default: the data holds still so the user's attention can move.
Passes when
- A data grid with names left-aligned, a right-aligned Amount column in
tabular-nums, a sticky header, and a "No entries yet" empty state. - An attendance table whose Present / Absent counts are right-aligned and aligned with their headers, scrolling under a pinned header row.
- A short reference table (no scroll) with correct type-based alignment and semantic
<th>s, no sticky header needed.
Fails when
- An amounts column centre-aligned or in proportional figures, so the ones and tens places wander down the column (TYP-5).
- A long roster whose header row scrolls off, leaving "which column is this?" once the user scrolls down.
- Rows of records built from
<div>s with no<table>/<th>semantics (A11Y-7), so AT users lose the row/column model. - A list that can be empty rendering as a blank panel with no empty-state copy (CMP-3).
How to verify
Static half: checks/structure-scan.py (the structure check) flags a <table> or a
role="table" element whose subtree holds no <th> and no role="columnheader". It is
the same walk A11Y-7 uses, and it reports the finding under both ids.
Alignment and tabular figures are never decided by script. A source scan cannot see which figures line up in a rendered column, and this control's own evaluator guidance exempts a deliberately left-aligned identifier column, a distinction no source scan can make. Check alignment against the rendered table and label it "verified manually".
Judgment half: the evaluator confirms numeric columns are right-aligned in tabular figures, the header stays visible while scrolling, the density fits the task (LAY-5), and the empty/loading states exist and read clearly (CMP-3). The evaluator also judges pattern fit: is a table the right pattern, or should this be cards/a list (SLP-11)?
Evaluator guidance
Flag: numeric columns that are not right-aligned or not tabular (digits fail to line up in the screenshots); a scrolled table with no visible header; div/card "tables" where a real table belongs; a table that can be empty or slow with no designed state.
Do not flag:
- A single-row or very short table with no scroll — no sticky header required.
- A deliberately left-aligned numeric column where the number is an identifier read as text (a reference code, a phone number), not a quantity to compare — note the reason.
- Cards or a list used for genuinely non-tabular content (records not compared across shared columns) — that is correct pattern choice, not a table violation.
This is L2: a reasoned deviation (an identifier column, a deliberately card-based layout
for non-tabular records) takes an inline dx-waive CMP-6 reason="…".