Let a rail hide when collapsed instead of narrowing

Plan step 25 (navigation Missing): M3's "Expanded behavior | Hide when
collapsed" was only implicit below `medium` in the adaptive rail.
`<x-navigation-rail hide-when-collapsed>` (collapsible and adaptive rails) takes
the rail out of the layout when it is collapsed; `$store.rail.show()` from an
app-bar menu button brings it back expanded over a scrim, sliding in, and its
own menu button docks it again. It does not reach the bands where the window,
not the visitor, collapses a rail (below `medium` for collapsible, `medium` for
adaptive), since M3's collapsed rail may never hide. Every `rail-collapsed`
branch now stops while the rail is open. `<x-app-shell hide-rail-when-collapsed>`
exposes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 07:15:20 +02:00
co-authored by Claude Opus 5
parent a5acbe7f6b
commit 9c047a8fb1
8 changed files with 227 additions and 28 deletions
+84 -4
View File
@@ -48,12 +48,14 @@
* in the rail: those have to beat a utility, and a layer never does.
*/
/* Every branch below reads `:not([data-open])`: a rail open over a scrim is drawn expanded whatever
made it collapsed, which only `hide-when-collapsed` can bring about outside the modal modes. */
@custom-variant rail-collapsed {
&:where([data-navigation-rail='collapsed'], [data-navigation-rail='collapsed'] *) {
@slot;
}
&:where([data-rail='collapsed'] [data-navigation-rail='collapsible'], [data-rail='collapsed'] [data-navigation-rail='collapsible'] *) {
&:where([data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]) *) {
@slot;
}
@@ -64,7 +66,7 @@
/* Below `medium` a 220360px rail would be most of the screen, so a rail whose width is the
visitor's choice is held collapsed there whatever they chose. */
@media (width < 37.5rem) {
&:where([data-navigation-rail='collapsible'], [data-navigation-rail='collapsible'] *) {
&:where([data-navigation-rail='collapsible']:not([data-open]), [data-navigation-rail='collapsible']:not([data-open]) *) {
@slot;
}
}
@@ -80,14 +82,14 @@
`data-rail-auto`, which <x-theme-script> sets while nothing is stored, stands for "no
choice yet" and the class's own default applies. */
@media (52.5rem <= width < 75rem) {
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'], :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive'] *) {
&:where(:is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]), :is([data-rail='collapsed'], [data-rail-auto]) [data-navigation-rail='adaptive']:not([data-open]) *) {
@slot;
}
}
/* Large and extra-large (from 1200): expanded to begin with, which is what M3 prefers there. */
@media (width >= 75rem) {
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive'], [data-rail='collapsed'] [data-navigation-rail='adaptive'] *) {
&:where([data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]) *) {
@slot;
}
}
@@ -239,6 +241,18 @@
--navigation-rail-collapsed-width: 5rem;
}
/* "Expanded behavior | Hide when collapsed" (the reference's Navigation rail § Variants and
configurations): instead of narrowing, the rail leaves the layout entirely — for an
immersive page — and comes back over it when a menu button somewhere else, the app bar's,
calls `$store.rail.show()`. It is the *expanded* rail that hides, which is why what comes
back is the expanded rail: "collapsed rail may not hide", and the two bands where a window
size class rather than the visitor collapses one are given back below. */
[data-navigation-rail][data-hide-when-collapsed] {
@variant rail-collapsed {
width: 0;
}
}
/* There is no room for a label under the 56px indicator at 80px, so a narrow rail is the icons
alone. The label is taken out of the drawing, not out of the page: it is what names the
destination for a screen reader. */
@@ -381,6 +395,72 @@
}
}
/* A rail that hides when collapsed is in the same position: nothing is left in the layout for
it to grow out of, so it takes the compact window's slide-in. The whole treatment is under
the `rail-collapsed` variant, which stops the moment the rail is open — that is what makes
it the expanded rail that comes back, on the rule above. */
[data-navigation-rail][data-hide-when-collapsed] > [data-navigation-rail-panel] {
@variant rail-collapsed {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate),
display var(--md-sys-motion-effects-default-duration) allow-discrete;
&:dir(rtl) {
translate: 100% 0;
}
}
}
[data-navigation-rail][data-hide-when-collapsed][data-open] > [data-navigation-rail-panel] {
display: flex;
translate: 0 0;
transition:
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate),
display var(--md-sys-motion-spatial-default-duration) allow-discrete;
@starting-style {
translate: -100% 0;
}
&:dir(rtl) {
translate: 0 0;
@starting-style {
translate: 100% 0;
}
}
}
/* The two bands the configuration does not reach, because there it is the window and not the
visitor that collapses the rail: below `medium` for a collapsible rail — where a rail is
held at its collapsed width anyway — and `medium` itself for the adaptive one, which M3
gives a collapsed rail. (Below `medium` the adaptive rail is already away, over the page,
and the block above it says so.) resources/js/navigation.js reads the same two numbers, so
the menu button and the drawing agree at every width. */
@media (width < 37.5rem) {
[data-navigation-rail='collapsible'][data-hide-when-collapsed] {
width: var(--navigation-rail-collapsed-width);
}
[data-navigation-rail='collapsible'][data-hide-when-collapsed] > [data-navigation-rail-panel] {
display: flex;
translate: none;
}
}
@media (37.5rem <= width < 52.5rem) {
[data-navigation-rail='adaptive'][data-hide-when-collapsed] {
width: var(--navigation-rail-collapsed-width);
}
[data-navigation-rail='adaptive'][data-hide-when-collapsed] > [data-navigation-rail-panel] {
display: flex;
translate: none;
}
}
[data-navigation-rail-scrim] {
position: fixed;
inset: 0;