Files
livewire-material/resources/css/components/navigation-rail.css
T
Andreas Reinhold / reiniandClaude Opus 5 92e560bef6 Slide the navigation rail and fade its scrim out in Firefox too
A rail that was open over the page left it on `transition: … display …
allow-discrete`: the compact adaptive rail's slide-out, the slide-out of
a rail that hides when collapsed, and every rail scrim's fade. Firefox
does not transition `display`, even with `allow-discrete` (Chrome 117
and Safari 18 do), so there the panel and the scrim vanished on the
first frame. Unlike the sheets' scrims these are drawn by attribute
rules on `data-md-open`, not `x-show`, so Alpine had nothing to hold.

navigation.js now marks a rail that closes `data-md-closing` — `sheet`
when the panel leaves the window, `scrim` when it stands in the layout
again — and drops it once the panel's `translate` and the scrim's
`opacity` transitions have finished (at once under reduced motion, or
when the rail opens again). navigation-rail.css keeps what is leaving
displayed, and a sliding panel in its open geometry, while the
attribute is set, and no longer transitions `display` anywhere, so
Chrome and Safari do not hold a second time. The view sets it from
`x-effect`, beside the `x-bind` that drops `data-md-open`, so both
attributes change in one flush: a `$watch` a microtask later let a
style read in between settle the scrim as already hidden. The collapsed
branches and `--md-navigation-rail-value` are untouched.

NavigationTest samples each exit part-way in the page (the compact
slide, the hide-when-collapsed slide, a modal rail's scrim) and checks
it ends hidden; all three fail on main in Firefox and pass in Chrome,
Firefox and Safari. They wait for the entry to finish first: Firefox
creates a transition on its next refresh tick, so straight after a
change its computed style already reads the end value. NavigationRailTest
pins the holds and that no rule transitions `display`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:24:43 +02:00

1058 lines
45 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* `<x-navigation-rail>`: M3 Expressive's navigation rail, collapsed (96px, icon over label) or
* expanded (a full-width pill, icon beside label) down the start edge of a `medium` or wider
* window (NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt, androidx Compose
* Material 3, Apache-2.0, and the layout of WideNavigationRail.kt;
* docs/reference/m3/components-navigation-selection-inputs.md § Navigation Rail).
*
* [data-md-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-md-open
* data-md-closing="sheet|scrim", while it exits
* [data-md-navigation-rail-scrim] modal and adaptive rails, open or closing only
* [data-md-navigation-rail-panel] the <nav>
* [data-md-navigation-rail-header] never scrolls
* [data-md-navigation-rail-menu-row] the menu button and the brand
* [data-md-navigation-rail-menu] md-state-layer md-focus-ring md-touch-target
* [data-md-navigation-rail-brand]
* [data-md-navigation-rail-fab-row] one <x-fab>, morphed (see the footer note)
* [data-md-navigation-rail-destinations] scrolls when the window is too short
* [data-md-navigation-rail-section] navigation-rail-section.css
* [data-md-navigation-rail-item] navigation-rail-item.css
* [data-md-navigation-rail-footer] never scrolls
*
* **Collapsed**: a rail (and anything in it) is drawn collapsed while any of these hold, each its
* own rule below wherever a property differs by shape — a fixed `collapsed` mode; a `collapsible` rail the visitor collapsed
* (`<html data-rail="collapsed">`, set before the first paint by `<x-theme-script>`) and not
* currently open over the page; a `modal` rail not open; below `medium` (600px) a `collapsible`
* rail regardless of choice, M3's floor ("compact → use a navigation bar, not a standard rail");
* below `expanded` (840px) an `adaptive` rail not open — there is no standard rail below it; at
* `expanded` (8401199) an `adaptive` rail not open, when the visitor chose collapsed *or* has not
* chosen (`data-rail-auto`, which `data-rail` alone cannot say, since it also carries
* `rail.default` for a visitor who never chose); from `large` (1200) an `adaptive` rail not open,
* only when the visitor chose collapsed — M3 starts it expanded there by default. Every branch but
* the first reads `:not([data-md-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.
* `resources/js/navigation.js` reads the same numbers, so the menu button and the drawing agree at
* every width.
*
* **Closing**: nothing here transitions `display`, which Firefox cannot do even with
* `allow-discrete`, so a panel sliding off the window and a fading scrim were cut to nothing there.
* `resources/js/navigation.js` sets `data-md-closing` on a rail that was open over the page until
* its exit transitions have run — `sheet` when the panel leaves the window (a compact adaptive
* rail, or one that hides when collapsed), `scrim` when the panel stands in the layout again — and
* the rules near the end of the file keep what is leaving displayed meanwhile, in every engine.
* The collapsed branches do not read it: a closing rail is already collapsed, which is what
* `--md-navigation-rail-value` says the moment it starts to close.
*
* The first set of branches also publishes the answer: `--md-navigation-rail-value` is `expanded`
* on every rail and `collapsed` wherever those branches hold — M3's two rail values, Compose's
* WideNavigationRailValue — so what an application puts in a rail reads it with
* `@container style(--md-navigation-rail-value: collapsed)` rather than copying the conditions. It
* is an unregistered custom property, so it inherits into every descendant and is unset outside a
* rail, where neither value matches.
*
* `data-md-width="narrow"` is M3's other collapsed width, `NarrowContainerWidth` 80px against the
* default 96 (`CollapsedContainerWidth`) — a variable, so every rule below that has its own reason
* to name a collapsed width does not have to know which one applies.
*
* `data-md-divider` and `data-md-fill="false"` are both Missing until this rewrite: an optional
* vertical divider on the page's edge, M3's own answer to a page scrolling under a fixed rail, and
* a transparent container as long as the items keep 3:1 contrast (N-22). `data-md-align="center"`
* is M3's other alignment, preferred on a tablet for reach — only the destinations move; the
* header, footer and menu button stay at their ends, and `safe` gives the top back once there are
* more destinations than fit.
*
* The rail-header FAB rules at the end draw M3's nested-FAB elevation (level 0, not a standalone
* FAB's 3 — N-03) and morph it into an extended FAB as the rail opens, rather than swapping two by
* `display` (N-23). Both sit in this file's own `material.components` layer and win by specificity
* alone, whichever copy of fab.css a build places last: the flat shadow outranks fab.css's resting
* and hover shadows, and the collapsed FAB's `gap` and minimum width outrank fab.css's extended-size
* and collapse-on-scroll rules, which would otherwise tie with them.
*
* The destination item, its indicator and its state layer are navigation-rail-item.css and
* navigation-item.css (shared with the bar's item); a section's heading is
* navigation-rail-section.css.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@import './fab.css';
@layer material.components {
[data-md-navigation-rail] {
--navigation-rail-expanded-width: clamp(220px, var(--navigation-rail-width, 256px), 360px);
--navigation-rail-collapsed-width: 96px;
--md-navigation-rail-value: expanded;
position: relative;
flex-shrink: 0;
width: var(--navigation-rail-expanded-width);
transition: width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
width: var(--navigation-rail-collapsed-width);
--md-navigation-rail-value: collapsed;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
width: var(--navigation-rail-collapsed-width);
--md-navigation-rail-value: collapsed;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
width: var(--navigation-rail-collapsed-width);
--md-navigation-rail-value: collapsed;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
width: var(--navigation-rail-collapsed-width);
--md-navigation-rail-value: collapsed;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
width: var(--navigation-rail-collapsed-width);
--md-navigation-rail-value: collapsed;
}
}
}
[data-md-navigation-rail][data-md-width='narrow'] {
--navigation-rail-collapsed-width: 80px;
}
[data-md-navigation-rail][data-md-hide-when-collapsed] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
width: 0;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
width: 0;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
width: 0;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
width: 0;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
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 (navigation-rail-item.css
renders it always — it is what names the destination). */
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-item] [data-md-navigation-label] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
}
/* A modal rail keeps its collapsed width in the layout while it is open over it, as Compose's
ModalWideNavigationRail does; the adaptive rail does below `expanded`, and takes no room at
all below `medium`. */
[data-md-navigation-rail='modal'] {
width: var(--navigation-rail-collapsed-width);
}
@media (width < 840px) {
[data-md-navigation-rail='adaptive'] {
width: var(--navigation-rail-collapsed-width);
}
}
@media (width < 600px) {
[data-md-navigation-rail='adaptive'] {
width: 0;
}
/* "Compact → don't use a standard rail, use a navigation bar." A `collapsible` rail takes
the width the visitor chose, which on a 360px screen would be two-thirds of it, so it is
floored at its collapsed width (N-24). `collapsed` and `expanded` mean what they say and
are left alone: wrap one in an application's own `medium:` element if it must not show
on a phone. */
[data-md-navigation-rail='collapsible'] {
width: var(--navigation-rail-collapsed-width);
}
}
/* `--material-banner` is the height of a bar pinned across the top of the window — what
<x-scaffold>'s `banner` slot holds, if the application made it sticky. The rail sticks under
it rather than behind it; 0 without one. */
[data-md-navigation-rail-panel] {
position: sticky;
top: var(--material-banner, 0px);
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
max-height: calc(100dvh - var(--material-banner, 0px));
overflow-x: clip;
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
transition:
width var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast),
background-color var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
}
/* M3's two optional container treatments: "optional vertical divider separates rail from
content, placed on the content-adjacent edge" — also its answer to a page scrolling under a
fixed rail — and "container fill can be turned off (transparent) as long as items keep ≥3:1
contrast" (N-22). Neither applies to a rail open over a scrim, a surface over the page. */
[data-md-navigation-rail][data-md-divider]:not([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
}
[data-md-navigation-rail][data-md-fill='false']:not([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
background-color: transparent;
}
/* Open: expanded over a scrim, in surface-container with a large corner at its inner edge — and
still, while a panel that is leaving the window slides off it (`data-md-closing="sheet"`). */
[data-md-navigation-rail]:is([data-md-open], [data-md-closing='sheet']) > [data-md-navigation-rail-panel] {
position: fixed;
inset-block: 0;
inset-inline-start: 0;
z-index: 50;
width: var(--navigation-rail-expanded-width);
max-width: calc(100vw - 56px);
height: 100dvh;
max-height: none;
border-start-end-radius: var(--md-sys-shape-corner-lg);
border-end-end-radius: var(--md-sys-shape-corner-lg);
background-color: var(--md-sys-color-surface-container);
box-shadow: var(--md-sys-elevation-2);
}
/* On a compact window there is no collapsed rail to grow out of: the open rail slides in from
the edge, on emphasized decelerate rather than a spring, which would overshoot and open a
gap. */
@media (width < 600px) {
[data-md-navigation-rail='adaptive'] > [data-md-navigation-rail-panel] {
position: fixed;
inset-block: 0;
inset-inline-start: 0;
z-index: 50;
display: none;
width: var(--navigation-rail-expanded-width);
max-width: calc(100vw - 56px);
height: 100dvh;
max-height: none;
border-start-end-radius: var(--md-sys-shape-corner-lg);
border-end-end-radius: var(--md-sys-shape-corner-lg);
background-color: var(--md-sys-color-surface-container);
box-shadow: var(--md-sys-elevation-2);
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
[data-md-navigation-rail='adaptive'][data-md-open] > [data-md-navigation-rail-panel] {
display: flex;
translate: 0 0;
transition:
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate);
@starting-style {
translate: -100% 0;
}
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 0 0;
@starting-style {
translate: 100% 0;
}
}
}
}
/* 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 collapsed branches, which stop the moment the rail is open — that is what makes it the
expanded rail that comes back, on the unconditional rule below. */
[data-md-navigation-rail][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
translate: -100% 0;
transition:
translate var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-easing-emphasized-accelerate);
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 100% 0;
}
}
}
}
[data-md-navigation-rail][data-md-hide-when-collapsed][data-md-open] > [data-md-navigation-rail-panel] {
display: flex;
translate: 0 0;
transition:
translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-easing-emphasized-decelerate);
@starting-style {
translate: -100% 0;
}
&:is([dir='rtl'], [dir='rtl'] *) {
translate: 0 0;
@starting-style {
translate: 100% 0;
}
}
}
/* Closing: what leaves stays drawn while it animates out. The exits above transition `translate`
and the scrim's `opacity` only — not `display`, which Firefox cannot transition even with
`allow-discrete`, so the slide and the fade were cut to nothing there — and
resources/js/navigation.js sets `data-md-closing` on the rail until those transitions have
run: `sheet` when the panel leaves the window, `scrim` when only the scrim was over the page.
One attribute more than every collapsed branch, so a hold outranks the `display: none` those
branches draw. */
[data-md-navigation-rail][data-md-hide-when-collapsed][data-md-closing='sheet'] > [data-md-navigation-rail-panel] {
display: flex;
}
@media (width < 600px) {
[data-md-navigation-rail='adaptive'][data-md-closing='sheet'] > [data-md-navigation-rail-panel] {
display: flex;
}
}
/* The two bands the configuration above does not reach, because there it is the window and not
the visitor that collapses the rail: below `medium` for a collapsible rail — already held at
its collapsed width above — and `medium` itself for the adaptive one, which M3 gives a
collapsed rail (below `medium` the adaptive rail is already away, over the page). This file's
header and resources/js/navigation.js read the same two numbers. */
@media (width < 600px) {
[data-md-navigation-rail='collapsible'][data-md-hide-when-collapsed] {
width: var(--navigation-rail-collapsed-width);
}
[data-md-navigation-rail='collapsible'][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
display: flex;
translate: none;
}
}
@media (600px <= width < 840px) {
[data-md-navigation-rail='adaptive'][data-md-hide-when-collapsed] {
width: var(--navigation-rail-collapsed-width);
}
[data-md-navigation-rail='adaptive'][data-md-hide-when-collapsed] > [data-md-navigation-rail-panel] {
display: flex;
translate: none;
}
}
[data-md-navigation-rail-scrim] {
position: fixed;
inset: 0;
z-index: 40;
display: none;
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
opacity: 0;
transition: opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
}
[data-md-navigation-rail][data-md-closing] > [data-md-navigation-rail-scrim] {
display: block;
}
[data-md-navigation-rail][data-md-open] > [data-md-navigation-rail-scrim] {
display: block;
opacity: 1;
@starting-style {
opacity: 0;
}
}
/* 44px above the header (TopSpace), 40px under it (HeaderSpaceMinimum) — 32 here and 8 as the
destinations' own padding, which keeps the first item's focus ring inside the scroller. */
[data-md-navigation-rail-header] {
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 44px);
padding-bottom: 32px;
}
[data-md-navigation-rail-panel] > [data-md-navigation-rail-destinations]:first-child {
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 44px);
}
/* The menu button and the brand: 20px at the row's end always; 28px at its start with the
button, so it centres a 40px button in the default 96px collapsed rail (28 either side); 20px
without one. A narrow (80px) rail's button re-centres to 20px either side while collapsed —
expanded, the row always keeps 28, since a rail's *expanded* width never depends on `narrow`. */
[data-md-navigation-rail-menu-row] {
display: flex;
width: 100%;
min-width: 0;
align-items: center;
gap: 12px;
padding-inline-end: 20px;
padding-inline-start: 20px;
}
[data-md-navigation-rail-header]:has([data-md-navigation-rail-menu]) [data-md-navigation-rail-menu-row] {
padding-inline-start: 28px;
}
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-header]:has([data-md-navigation-rail-menu]) [data-md-navigation-rail-menu-row] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
padding-inline-start: 20px;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
padding-inline-start: 20px;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
padding-inline-start: 20px;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
padding-inline-start: 20px;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
padding-inline-start: 20px;
}
}
}
[data-md-navigation-rail-menu] {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--md-sys-shape-corner-full);
color: var(--md-sys-color-on-surface-variant);
cursor: pointer;
}
/* The two glyphs swap by `display: contents`/`none`, so the button's own size never depends on
which is shown, and the hidden one never announces to a screen reader (the button's own
aria-label already says what the click will do). */
[data-md-navigation-rail-menu-collapse-icon] {
display: contents;
}
[data-md-navigation-rail-menu-expand-icon] {
display: none;
}
[data-md-navigation-rail-menu-collapse-icon],
[data-md-navigation-rail-menu-expand-icon] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
display: none;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
display: none;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
display: none;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
}
}
}
[data-md-navigation-rail-menu-expand-icon] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
display: contents;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
display: contents;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
display: contents;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: contents;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: contents;
}
}
}
[data-md-navigation-rail-brand] {
min-width: 0;
flex: 1 1 0%;
}
[data-md-navigation-rail-brand] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
display: none;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
display: none;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
display: none;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
display: none;
}
}
}
/* The FAB row: 20px either side in the default 96px rail, 12px in a narrow 80 while collapsed
(the FAB itself is centred by the row's own width, not by this padding). */
[data-md-navigation-rail-fab-row] {
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding-inline: 20px;
}
[data-md-navigation-rail][data-md-width='narrow'] [data-md-navigation-rail-fab-row] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
padding-inline: 12px;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
padding-inline: 12px;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
padding-inline: 12px;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
padding-inline: 12px;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
padding-inline: 12px;
}
}
}
[data-md-navigation-rail-destinations] {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding-block: 8px;
scrollbar-width: thin;
}
[data-md-navigation-rail-destinations],
[data-md-navigation-rail-footer] {
display: flex;
flex-direction: column;
}
[data-md-navigation-rail-destinations],
[data-md-navigation-rail-footer] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
gap: 4px;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
gap: 4px;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
gap: 4px;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
gap: 4px;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
gap: 4px;
}
}
}
[data-md-navigation-rail-footer] {
flex-shrink: 0;
padding-block: 8px 16px;
}
/* "Alignment: top or center; on tablets prefer center alignment for reach. Menu icon and FAB
are always top-aligned" (§ Navigation Rail/Behaviour). Only the destinations move — the
header and the footer stay at their ends — and `safe` puts them back at the top the moment
there are more of them than fit, where centring would otherwise push the first one out of
reach above the scroller. */
[data-md-navigation-rail][data-md-align='center'] [data-md-navigation-rail-destinations] {
justify-content: safe center;
}
/*
* A FAB in the rail's header rests at elevation 0, not the 3 a standalone FAB has
* (§ Navigation Rail/Behaviour: "when nested within another component, such as the navigation
* rail, the FAB's resting elevation should be level 0" — N-03). The doubled `[data-md-fab]`
* attribute only adds specificity, over fab.css's `[data-md-fab]:hover` too, as
* `[data-md-navigation-rail-item][data-md-navigation-rail-item]::before`
* (navigation-rail-item.css) does for the same reason.
*/
[data-md-navigation-rail-header] [data-md-fab][data-md-fab],
[data-md-navigation-rail-header] [data-md-fab][data-md-fab]:hover {
box-shadow: none;
}
/*
* "Rail transitions from collapsed to expanded should animate the FAB into and out of an
* extended FAB" (N-23) — so the header takes one `<x-fab label icon>` and morphs it, rather
* than swapping two by `display`, which showed the collapsed one at once while the rail's
* width was still springing. The label's width springs on the rail's own spatial spring; with
* the gap and the extended FAB's own minimum width both zeroed, `aspect-ratio` squares what is
* left against the height its size already sets — 56, 80 or 96px, the three FAB sizes — so no
* number is repeated here. The label stays in the accessibility tree at both widths, so the FAB
* keeps its name. The cap is there because a transition needs a length to travel to; a rail
* FAB's label is a word or two.
*/
[data-md-navigation-rail-header] [data-md-fab] > span {
max-width: 256px;
overflow: hidden;
white-space: nowrap;
transition:
max-width var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default),
opacity var(--md-sys-motion-effects-default-duration) var(--md-sys-motion-effects-default);
}
[data-md-navigation-rail-header] [data-md-fab] > span {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
max-width: 0;
opacity: 0;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
max-width: 0;
opacity: 0;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
max-width: 0;
opacity: 0;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
max-width: 0;
opacity: 0;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
max-width: 0;
opacity: 0;
}
}
}
/* `[data-md-extended]` doubled for the same reason as the shadow's `[data-md-fab]` above: it
outranks fab.css's `[data-md-fab][data-md-extended][data-md-size]` gap and its collapsed
collapse-on-scroll FAB's minimum width, three attributes each. */
[data-md-navigation-rail-header] [data-md-fab][data-md-extended][data-md-extended] {
&:where(
[data-md-navigation-rail='collapsed'],
[data-md-navigation-rail='collapsed'] *,
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='collapsible']:not([data-md-open]) *,
[data-md-navigation-rail='modal']:not([data-md-open]),
[data-md-navigation-rail='modal']:not([data-md-open]) *
) {
min-inline-size: 0;
aspect-ratio: 1;
gap: 0;
}
@media (width < 600px) {
&:where([data-md-navigation-rail='collapsible']:not([data-md-open]), [data-md-navigation-rail='collapsible']:not([data-md-open]) *) {
min-inline-size: 0;
aspect-ratio: 1;
gap: 0;
}
}
@media (width < 840px) {
&:where([data-md-navigation-rail='adaptive']:not([data-md-open]), [data-md-navigation-rail='adaptive']:not([data-md-open]) *) {
min-inline-size: 0;
aspect-ratio: 1;
gap: 0;
}
}
@media (840px <= width < 1200px) {
&:where(
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]),
:is([data-rail='collapsed'], [data-rail-auto]) [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
min-inline-size: 0;
aspect-ratio: 1;
gap: 0;
}
}
@media (width >= 1200px) {
&:where(
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]),
[data-rail='collapsed'] [data-md-navigation-rail='adaptive']:not([data-md-open]) *
) {
min-inline-size: 0;
aspect-ratio: 1;
gap: 0;
}
}
}
}