From 42d62ed00c57faadc37525fac8f299bdb7e9e284 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:08:13 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 2 +- resources/css/components/table.css | 28 +++++++++++-------- resources/views/components/table.blade.php | 12 +++++--- tests/Feature/Components/DataTest.php | 5 ++++ 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index c3f235e1..85243b34 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -831,7 +831,7 @@ A choice of colour profile (see Colour profiles): a swatch per generated profile ### ``, `` -A data table: write plain ``, ``, ``, `` inside `` (`size="xs"` for a dense one); cell utilities (`text-end`, `whitespace-nowrap`) always win. Scrolling is yours: wrap it in `
`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`. +A data table: write plain ``, ``, ``, `` inside ``; cell utilities (`text-end`, `whitespace-nowrap`) always win. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in `
`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`. `Size` sorts through the Livewire property `sortBy` (`['column' => …, 'direction' => 'asc'|'desc']`; `model` names another), with `aria-sort`. diff --git a/resources/css/components/table.css b/resources/css/components/table.css index d2a82a80..17feaa5b 100644 --- a/resources/css/components/table.css +++ b/resources/css/components/table.css @@ -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) { diff --git a/resources/views/components/table.blade.php b/resources/views/components/table.blade.php index c8beea84..1d15cd9c 100644 --- a/resources/views/components/table.blade.php +++ b/resources/views/components/table.blade.php @@ -2,15 +2,19 @@ look is resources/css/components/table.css, keyed on the `data-table` attribute set here (the top of that file says why). - `size="xs"` for a table inside a panel inside a panel. A selected row is - ``. Horizontal scrolling stays the caller's — wrap the table in - `
` where the page needs it. A column that sorts is + Rows are 52px, a target a finger can hit. `dense` tightens them to 36px — M3 asks that density + always be an opt-in and never a default, so it is the caller's decision and the caller's to + justify. `size="xs"` for a table inside a panel inside a panel (32px rows, 24px with `dense`). + + A selected row is ``. Horizontal scrolling stays the caller's — wrap the + table in `
` where the page needs it. A column that sorts is ``. --}} @props([ 'size' => 'sm', + 'dense' => false, ]) - +
{{ $slot }}
diff --git a/tests/Feature/Components/DataTest.php b/tests/Feature/Components/DataTest.php index 97801759..8669f34c 100644 --- a/tests/Feature/Components/DataTest.php +++ b/tests/Feature/Components/DataTest.php @@ -13,6 +13,11 @@ it('marks a table for its styles and takes a size', function () { ->and((string) $this->blade(''))->toContain('data-size="sm"'); }); +it('tightens a table only when the caller asks for it', function () { + expect((string) $this->blade(''))->not->toContain('data-dense') + ->and((string) $this->blade(''))->toContain('data-dense'); +}); + it('sorts by its column, ascending first and then flipping', function () { expect((string) $this->blade('Size')) ->not->toContain('aria-sort')