Ease a collapse open and shut in Firefox and Safari too

collapse.css animated `<details>`' `::details-content` from `block-size: 0`
to `auto`, which needs `interpolate-size: allow-keywords` (Chrome 129 and
nothing else) and a `content-visibility` held through the close with
`allow-discrete` (not Firefox), so in Firefox and Safari the section
snapped open and shut. No stylesheet can do it there.

resources/js/collapse.js animates the `<details>` itself, the same way
in every engine: a Web Animation of its `block-size` from the height it
is drawn at to the height it is going to, on the fast spatial spring's
tokens, with `overflow: clip` and a `min-block-size` that keeps the
summary whole through the spring's overshoot, all inside the keyframes,
so nothing is left in `style`. Content below moves with it. Opening sets
`open` at once and grows the height; closing keeps `open` while the
height goes, marks `data-md-collapse-closing` so the chevron turns back
at the start, and drops `open` (and fires `toggle`) when the height has.
A press mid-way turns it round from the height it has reached. The CSS
animation is gone, so Chrome takes the same path and never animates
twice.

Routes: a press on the summary (pointer, Enter, Space) is taken over;
the Alpine and Livewire bindings call `materialCollapse()` from the
view's `x-effect` (its first call, as Alpine starts, sets the state at
once); a `name` group's open member closes on the same spring, its
`name` lifted for the close so the browser's exclusivity does not shut
it first and put back once closed. Anything else that sets `open`
(find-in-page, an application's own script) stays instant, as the
browser draws it, and is followed. Under reduced motion every route is
the browser's own.

Browser tests in Chrome, Firefox and Safari catch the height part-way
open and shut (and what is under it moving) from a press and from the
Alpine binding, the chevron turning at the start of a close, the clip
while it moves, Enter on the summary, a reversal mid-way, a name group,
and reduced motion. The four motion tests fail on main in Firefox and
Safari.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-16 21:00:23 +02:00
co-authored by Claude Opus 5
parent 15e4fa7ed1
commit 9f46630352
6 changed files with 486 additions and 28 deletions
+8 -23
View File
@@ -4,12 +4,13 @@
* state through a Livewire morph, and is announced as a disclosure by the browser. Judged against
* foundations rather than any M3 component spec (docs/audits/m3-alignment/containment.md, C-24).
*
* `interpolate-size: allow-keywords` makes `<details>`'s `0 -> auto` block-size an animatable pair,
* so `::details-content` (the pseudo-element that holds everything after the summary) can transition
* `block-size` on the fast spatial spring, the same one that turns the chevron; `content-visibility`
* goes with it, `allow-discrete`, so the content stays rendered while it closes rather than
* vanishing on the first frame. Under reduced motion the duration token is zero
* (tokens/motion.css), so the section snaps open with nothing else to do.
* The height eases open and shut on the fast spatial spring, the same one that turns the chevron,
* from resources/js/collapse.js: it animates the `<details>`' own `block-size`. This file cannot —
* `interpolate-size` (for `0 -> auto`) is Chrome's alone, and Firefox does not hold the content's
* `content-visibility` through a close — so Firefox and Safari used to snap open and shut. While a
* close runs, `open` is still set and `data-md-collapse-closing` turns the chevron back at its
* start. Under reduced motion the duration token is zero (tokens/motion.css), and the section
* snaps, as the browser draws it.
*
* `data-md-variant="filled"` is a surface-container tile with a large corner; the summary inherits
* it (`border-radius: inherit`) so the shared `md-state-layer`'s own `::before`, which also
@@ -27,27 +28,11 @@
@import './icon.css';
@layer material.components {
[data-md-collapse] {
interpolate-size: allow-keywords;
}
[data-md-collapse][data-md-variant='filled'] {
border-radius: var(--md-sys-shape-corner-lg);
background-color: var(--md-sys-color-surface-container);
}
[data-md-collapse]::details-content {
block-size: 0;
overflow: hidden;
transition:
block-size var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast),
content-visibility var(--md-sys-motion-spatial-fast-duration) allow-discrete;
}
[data-md-collapse][open]::details-content {
block-size: auto;
}
[data-md-collapse-summary] {
display: flex;
min-block-size: var(--md-sys-measurement-space600);
@@ -82,7 +67,7 @@
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
}
[data-md-collapse][open] > [data-md-collapse-summary] > [data-md-collapse-chevron] {
[data-md-collapse][open]:not([data-md-collapse-closing]) > [data-md-collapse-summary] > [data-md-collapse-chevron] {
rotate: 180deg;
}