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.

Typischer Versatz und was er kostet
Betriebsart Versatz Geeignet für
Batch1–24 hBerichte, Abrechnung, Trends
Micro-Batch1–5 minZustandsüberwachung, Alarme
Streaming< 1 sRegelung, 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>
The caption is the title. Not a heading above the block. A <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.

Angebundene Quellen, Stand Q2
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.

Vier Spalten in 22 rem
Anlage Standort Verantwortlich Inbetriebnahme
Linie 1Werk SüdInstandhaltung 1998
Linie 2Werk SüdInstandhaltung 2014
The three attributes are not optional decoration. A scroll box whose children are all plain text has nothing focusable inside it, so a keyboard has no way to reach it and no way to move it: the last column simply does not exist for anyone not holding a mouse. That is a failure of SC 2.1.1 Keyboard, level A, and it is the single most common accessibility defect in responsive table markup. 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.

Dieselbe Tabelle, invertierte Fläche
Kategorie Rechtsgrundlage Dauer
Notwendig§ 25 Abs. 2 TDDDG12 Monate
StatistikEinwilligung13 Monate
MarketingEinwilligung12 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 / elementElementDoes
.cf-table__scrolldivThe scroll box. Carries tabindex="0", role="region" and aria-labelledby. Focus ring comes from base.css, which rings any [tabindex].
.cf-tabletableFull width, collapsed borders, 14 px, tabular figures.
<caption>captionMono, 12 px, --text-secondary, left, --space-3 above the head rule. The accessible name of both the table and the region.
th[scope="col"]thMono 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"]thBody face, body size, medium weight, full ink. The row's subject, not a label.
tdtd--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__numth / tdRight-aligns a column of figures. Goes on the head cell too.
last cellth / tdDrops 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

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__num on 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.