Make a table's density the caller's decision

Density was applied to every fine-pointer device with no way out, which
M3 forbids outright: "don't apply density by default; offer an explicit
density opt-in instead, keeping opt-out targets at >= 48x48 CSS px". A
row is now 52px and `dense` is a prop that tightens it to 36. The row
divider also takes outline-variant itself rather than 60% of it, since M3
keeps opacity for state layers and disabled.

Plan step 20, findings IN-16 and IN-30.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:08:13 +02:00
co-authored by Claude Fable 5.1
parent 5989e9ea92
commit 42d62ed00c
4 changed files with 30 additions and 17 deletions
+16 -12
View File
@@ -5,9 +5,14 @@
* So the styling is descendant selectors on the one attribute the component sets, all inside
* `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always
* wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in
* body-medium between faint rules. One density step tighter on a fine pointer — keyed on the
* pointer, not the width, so a touch tablet in landscape keeps rows a finger can hit. A row that
* opens something is `data-list-row` and answers a pointer as a list row does (components/list.css).
* body-medium between outline-variant rules. A row that opens something is `data-list-row` and
* answers a pointer as a list row does (components/list.css).
*
* A row is 52px: 16px above and below a body-medium line. Density is never applied by itself —
* "don't apply density by default; offer an explicit density opt-in instead, keeping opt-out targets
* at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md § Accessibility) — so `dense`
* (`data-dense`) is the caller's decision, and it is the caller's to justify: a dense row is 36px,
* and a dense row inside an `xs` table 24px.
*/
@layer components {
@@ -16,7 +21,7 @@
viewport. */
[data-table] {
--cell-x: 0.75rem;
--cell-y: 0.75rem;
--cell-y: 1rem;
position: relative;
width: 100%;
@@ -34,14 +39,12 @@
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
}
@media (pointer: fine) {
[data-table] {
--cell-y: 0.5rem;
}
[data-table][data-dense] {
--cell-y: 0.5rem;
}
[data-table][data-size="xs"] {
--cell-y: 0.25rem;
}
[data-table][data-size="xs"][data-dense] {
--cell-y: 0.25rem;
}
[data-table] :where(th, td) {
@@ -63,8 +66,9 @@
letter-spacing: var(--md-sys-typescale-label-md-tracking);
}
/* The role itself, not a fraction of it: M3 reserves opacity for state layers and disabled. */
[data-table] :where(tbody tr) {
border-bottom: 1px solid color-mix(in srgb, var(--md-sys-color-outline-variant) 60%, transparent);
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
[data-table] :where(tbody tr:last-child) {