Components
Table
Ruled rows and nothing vertical. A stack of hairlines with mono labels over it — the
same three marks the section header and the accordion are made of, arranged to be read
across instead of down. Plain <table> markup, no script, no
row-as-a-div.
Default
A caption, a head row of mono labels on the strong hairline, body rows on the subtle one. Everything else is the browser's own table layout, which is better at sizing columns than any set of widths we could write.
| Betriebsart | Versatz | Geeignet für |
|---|---|---|
| Batch | 1–24 h | Berichte, Abrechnung, Trends |
| Micro-Batch | 1–5 min | Zustandsüberwachung, Alarme |
| Streaming | < 1 s | Regelung, Netzstabilität |
<div class="cf-table__scroll" tabindex="0" role="region" aria-labelledby="tbl-versatz"> <table class="cf-table"> <caption id="tbl-versatz">Typischer Versatz und was er kostet</caption> <thead> <tr> <th scope="col">Betriebsart</th> <th scope="col">Versatz</th> <th scope="col">Geeignet für</th> </tr> </thead> <tbody> <tr><td>Batch</td><td>1–24 h</td><td>Berichte, Abrechnung, Trends</td></tr> </tbody> </table> </div>
<caption> is the table's accessible name, it is announced when a
screen reader lands on the table, and it stays attached to it when the table is moved,
quoted or printed. A heading floating above the box is none of those things — it is
only near it.
Row headers and numeric columns
When the first cell of a row names what the rest of the row is about, it is a header —
<th scope="row">, not a <td>. It is drawn in the
body face at body size rather than as a mono label: a row header is the row's subject,
which is content, and the mono face in this system marks the things that are
not prose.
Any column of figures takes .cf-table__num on the head cell and on every
body cell in it. That right-aligns the column so the digits stack, which is the whole
reason the table sets tabular figures in the first place.
| Quelle | Protokoll | Signale | Takt (ms) |
|---|---|---|---|
| Presse 04 | OPC UA | 1.284 | 250 |
| Trockner Nord | Modbus TCP | 96 | 1.000 |
| Historian | SQL | 12.470 | 60.000 |
<tr> <th scope="row">Presse 04</th> <td>OPC UA</td> <td class="cf-table__num">1.284</td> </tr>
When it is wider than its column
A table is the one block in this system that legitimately outgrows the measure, so it gets a scroll box rather than permission to widen the page. The demo below is capped at 22 rem to force the state at any window size — click it or tab to it, then use the arrow keys.
What stops it compressing instead of overflowing is that the column labels do not wrap. Without that floor a narrow table simply wraps every cell to three lines, stays inside its box, and the scroll it is wrapped in never engages — the mechanism would be present and permanently unused.
| Anlage | Standort | Verantwortlich | Inbetriebnahme |
|---|---|---|---|
| Linie 1 | Werk Süd | Instandhaltung | 1998 |
| Linie 2 | Werk Süd | Instandhaltung | 2014 |
tabindex="0" makes the box a tab stop and arrow-scrollable;
role="region" gives it a landmark to be announced as;
aria-labelledby pointed at the caption's id gives that landmark
its name, so the table is named once and the region borrows it rather than repeating it
in an aria-label that would then drift.
The cost is honest and worth stating: the box is a tab stop even on a wide screen where it does not scroll, so a keyboard user lands once on something that does nothing. The alternative is a script that measures overflow on load and on resize and adds the attribute conditionally, and this system does not spend a shipping script on a convenience. One redundant tab stop beats one unreachable column.
There is no card-per-row transformation at small widths. Turning rows into stacked cards
breaks the one thing a table is for — comparing a column down the page — and a
display: block table drops the row and column semantics that
scope was there to provide. The narrow answer is the same table, scrolled.
On a dark surface
No modifier. Every colour in the component is a semantic token, so the table inverts by
being put somewhere inverted — data-theme="inverse", the footer, or the
consent dialog, which rebinds the same four rungs itself.
| Kategorie | Rechtsgrundlage | Dauer |
|---|---|---|
| Notwendig | § 25 Abs. 2 TDDDG | 12 Monate |
| Statistik | Einwilligung | 13 Monate |
| Marketing | Einwilligung | 12 Monate |
The first real consumer is the cookie inventory the site owes under TDDDG § 25 and Art. 13 DSGVO — the list of what is stored, by whom, why and for how long. → Consent · The inventory
Anatomy
| Class / element | Element | Does |
|---|---|---|
.cf-table__scroll | div | The scroll box. Carries tabindex="0", role="region" and aria-labelledby. Focus ring comes from base.css, which rings any [tabindex]. |
.cf-table | table | Full width, collapsed borders, 14 px, tabular figures. |
<caption> | caption | Mono, 12 px, --text-secondary, left, --space-3 above the head rule. The accessible name of both the table and the region. |
th[scope="col"] | th | Mono 11 px uppercase on --border-strong. The section header's label, at cell scale. Does not wrap — the head row is what gives the table a minimum width, and without one it compresses instead of scrolling. |
th[scope="row"] | th | Body face, body size, medium weight, full ink. The row's subject, not a label. |
td | td | --space-3 above and below, --space-4 to the right, nothing to the left; top-aligned so a wrapping cell keeps its first line on the row's baseline. |
.cf-table__num | th / td | Right-aligns a column of figures. Goes on the head cell too. |
| last cell | th / td | Drops its right padding, so the row ends on the same axis as its own hairline. |
.cf-prose table is the same component under another name: an article's
tables are written as bare elements, so every selector above names
.cf-prose alongside .cf-table rather than being declared a
second time. There is one drawing, and it cannot drift.
→ Article & Prose
Accessibility
- Every header cell carries an explicit
scope. It is what tells assistive technology whether the cell heads a column or a row, and a table where the association has to be guessed from position is a table read out as a list of unlabelled values. - A
<caption>on every table, and it is the first child of<table>— that placement is required, not stylistic. - The scroll box is focusable, has
role="region", and takes its name from the caption. SC 2.1.1, level A. <thead>and<tbody>add no accessibility semantics of their own — they are here for printing and for the head rule.scopeis what does the work.- No
headers/idpairing anywhere. It exists for tables with several header levels, and a table that needs it is a table that should have been split. If two header rows seem necessary, the answer is two tables. - Nothing is conveyed by colour or by weight alone: the head is a different face and sits on a heavier rule, and the row header is distinguished by size and weight as well as position.
- Contrast, measured in the browser against CF-Grau — the dark end of the page wash, so these are the floors and every one of them improves as the wash lightens: body ink and mono head 13.5:1, caption
--text-secondary5.9:1. On the inverse surface the same three are 21:1, 21:1 and 10.5:1. The row rule is a separator rather than a UI contour: the structure is carried by the markup, not by the hairline, so it is not load-bearing for the 3:1 floor. - No layout tables. If it is not data, it is a grid.
Rules
Do
- Caption every table, in words that say what the rows are.
- Wrap it in the scroll box with all three attributes, every time — including when you are sure it fits.
- Make the first column a
th scope="row"whenever it names the row. - Put
.cf-table__numon any column of figures, head cell included. - Keep it to four or five columns. Past that it is a report, and a report is a download.
Don't
- No vertical rules and no boxed grid. The row rule is the whole drawing.
- No zebra striping. This system draws contours, not fills, and a stripe is a fill doing a hairline's job.
- No sorting, filtering or pagination inside the component — that is a server's work and a different pattern.
- Don't turn rows into cards on a phone. It breaks the column comparison the table exists for.
- Don't set column widths. The browser sizes them better, and a fixed width is the first thing to break in the other language.
- Don't spend lime in a cell. A table is data, not the screen's one light moment.