# CMP-6 — Data tables use the table pattern — semantic rows and headers, numeric columns right-aligned with tabular figures, text left-aligned, and a header that stays visible while scrolling

L2 · hybrid · Components & patterns

**Fails when:** numeric columns centre- or left-aligned, or in proportional figures, so values do not line up (TYP-5) · a long table whose header scrolls out of view, leaving columns unlabelled · tabular data rendered as stacked cards or divs instead of a real table (A11Y-7) · a table that can be empty or slow has no designed empty or loading state (CMP-3)

## 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="…"`.
