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
845 lines
36 KiB
CSS
845 lines
36 KiB
CSS
/*
|
||
* M3 Expressive navigation: the flexible navigation bar and the navigation rail — collapsed,
|
||
* expanded and modal.
|
||
*
|
||
* Values from androidx Compose Material 3 (Apache-2.0) at androidx-main
|
||
* 27cf9a7d5788aa0f5f2d8b6699ce279560daf326: tokens/NavigationBarTokens.kt,
|
||
* NavigationBarVerticalItemTokens.kt, NavigationBarHorizontalItemTokens.kt,
|
||
* NavigationRailCollapsedTokens.kt, NavigationRailExpandedTokens.kt,
|
||
* NavigationRailBaselineItemTokens.kt, NavigationRailVerticalItemTokens.kt,
|
||
* NavigationRailHorizontalItemTokens.kt, NavigationRailColorTokens.kt, and the layout in
|
||
* ShortNavigationBar.kt, WideNavigationRail.kt and NavigationItem.kt.
|
||
*
|
||
* [data-navigation-bar] surface-container, 64px (data-tall: 80px), the bottom
|
||
* safe area under it
|
||
* [data-navigation-bar-items] equal widths; centred from a 600px-wide short bar
|
||
* [data-navigation-bar-item] data-active
|
||
* [data-navigation-pill] icon and label; the indicator itself from 600px
|
||
* [data-navigation-indicator] the 56×32 indicator around the icon below 600px
|
||
* [data-navigation-label]
|
||
*
|
||
* [data-navigation-rail="collapsed|expanded|collapsible|modal|adaptive"] data-open
|
||
* [data-navigation-rail-scrim] modal and adaptive rails
|
||
* [data-navigation-rail-panel] the <nav>
|
||
* [data-navigation-rail-header] menu button, brand, FAB — never scrolls
|
||
* [data-navigation-rail-destinations] scrolls when the window is too short
|
||
* [data-navigation-rail-section] a heading (expanded only) and its items
|
||
* [data-navigation-rail-item] data-active; the full-width pill when expanded
|
||
* [data-navigation-indicator] the 56×32 indicator when collapsed
|
||
* [data-navigation-label]
|
||
* [data-navigation-rail-footer] never scrolls
|
||
*
|
||
* `rail-collapsed:` matches a rail, and everything in it, while it is drawn collapsed — whatever
|
||
* made it so: its mode, the visitor's choice on <html data-rail> (set before the first paint by
|
||
* <x-theme-script>), or a window below `expanded` (840px) for the adaptive rail. A rail item is written once
|
||
* and takes both shapes from it; so can anything an application puts in a rail
|
||
* (`<span class="rail-collapsed:hidden">`).
|
||
*
|
||
* The two widths the adaptive rail turns on are M3's window size classes, not Tailwind's screens
|
||
* (docs/reference/m3/foundations.md § Layout, foundations-supplement.md § Breakpoints): 37.5rem
|
||
* (600px) is the compact/medium boundary — below it the navigation bar carries the destinations and
|
||
* the rail takes no room in the layout; 52.5rem (840px) is `expanded`, where M3 asks for a standard
|
||
* rail in the layout rather than one that opens over a scrim; 75rem (1200px) is `large`, where the
|
||
* rail starts expanded instead of collapsed. The bar's own item layout is a *container* query at
|
||
* the same 37.5rem, so a bar in a narrow column lays out by its own width.
|
||
*
|
||
* Everything here is in `@layer components` except the blocks after it, which reach past the
|
||
* component into what an application wrote — the bottom offset <x-app-shell> publishes, a FAB put
|
||
* 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']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||
@slot;
|
||
}
|
||
|
||
&:where([data-navigation-rail='modal']:not([data-open]), [data-navigation-rail='modal']:not([data-open]) *) {
|
||
@slot;
|
||
}
|
||
|
||
/* Below `medium` a 220–360px 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']:not([data-open]), [data-navigation-rail='collapsible']:not([data-open]) *) {
|
||
@slot;
|
||
}
|
||
}
|
||
|
||
@media (width < 52.5rem) {
|
||
&:where([data-navigation-rail='adaptive']:not([data-open]), [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||
@slot;
|
||
}
|
||
}
|
||
|
||
/* Expanded (840–1199): a standard rail, collapsed unless the visitor expanded it. `data-rail`
|
||
alone cannot say that — it carries `rail.default` for a visitor who never chose — so
|
||
`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']: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']:not([data-open]), [data-rail='collapsed'] [data-navigation-rail='adaptive']:not([data-open]) *) {
|
||
@slot;
|
||
}
|
||
}
|
||
}
|
||
|
||
@layer components {
|
||
/* ---------------------------------------------------------------- the navigation bar */
|
||
|
||
[data-navigation-bar] {
|
||
container-type: inline-size;
|
||
padding-inline: var(--material-safe-left, env(safe-area-inset-left)) var(--material-safe-right, env(safe-area-inset-right));
|
||
padding-bottom: var(--material-safe-bottom, env(safe-area-inset-bottom));
|
||
background-color: var(--md-sys-color-surface-container);
|
||
color: var(--md-sys-color-on-surface-variant);
|
||
}
|
||
|
||
[data-navigation-bar-items] {
|
||
display: flex;
|
||
min-height: 4rem;
|
||
margin-inline: auto;
|
||
}
|
||
|
||
/* The tall configuration: NavigationBarTokens.TallContainerHeight, 80dp. It keeps the vertical
|
||
item layout at every width — the horizontal one below is the short bar's alone — so the
|
||
label always sits under its icon, which is what the extra 16px is for. */
|
||
[data-navigation-bar][data-tall] [data-navigation-bar-items] {
|
||
min-height: 5rem;
|
||
}
|
||
|
||
/* "Scrolling: hides on scroll-down, reappears on scroll-up" (the reference's Navigation bar
|
||
§ Behaviour). It slides out of the window on the default spatial spring, which reduced
|
||
motion zeroes to an instant swap along with every other duration token. Whether it may hide
|
||
at all is resources/js/navigation.js's call: never while a bottom sheet, a drawer or a
|
||
snackbar is on screen, since those are anchored to the bar's edge. */
|
||
[data-navigation-bar][data-hide-on-scroll] {
|
||
transition: translate var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||
}
|
||
|
||
[data-navigation-bar][data-hide-on-scroll][data-hidden] {
|
||
translate: 0 100%;
|
||
}
|
||
|
||
[data-navigation-bar-item] {
|
||
--navigation-layer: 0;
|
||
position: relative;
|
||
display: flex;
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding-block: 0.375rem;
|
||
color: var(--md-sys-color-on-surface-variant);
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
outline: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
[data-navigation-bar-item] [data-navigation-pill] {
|
||
display: flex;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
font: var(--md-sys-typescale-label-md);
|
||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||
}
|
||
|
||
[data-navigation-bar-item] [data-navigation-label] {
|
||
max-width: 100%;
|
||
padding-inline: 0.25rem;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
[data-navigation-bar-item][data-active] {
|
||
color: var(--md-sys-color-secondary);
|
||
}
|
||
|
||
/* From 600dp (M3's medium window), icon and label side by side in a 40px indicator, and the
|
||
items centred with the padding ShortNavigationBar's Centered arrangement computes. The
|
||
short bar only: a tall bar is the vertical layout at every width, so every rule here that
|
||
the vertical layout also has an opinion about names the short bar. */
|
||
@container (width >= 37.5rem) {
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items] {
|
||
width: calc(10% * (var(--navigation-bar-count, 7) + 3));
|
||
min-width: fit-content;
|
||
max-width: 100%;
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(1)) { --navigation-bar-count: 1; }
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(2)) { --navigation-bar-count: 2; }
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(3)) { --navigation-bar-count: 3; }
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(4)) { --navigation-bar-count: 4; }
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(5)) { --navigation-bar-count: 5; }
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-items]:has(> :last-child:nth-child(6)) { --navigation-bar-count: 6; }
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] {
|
||
min-width: max-content;
|
||
}
|
||
|
||
/* The label stays label-medium: NavigationBarTokens.LabelTextFont is the bar's only label
|
||
token, and Compose's ShortNavigationBarItem passes it for both icon positions. (The
|
||
*rail's* horizontal item is label-large — NavigationRailHorizontalItemTokens — which is
|
||
a different component.) */
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-pill] {
|
||
position: relative;
|
||
isolation: isolate;
|
||
flex-direction: row;
|
||
height: 2.5rem;
|
||
padding-inline: 1rem;
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-label] {
|
||
padding-inline: 0;
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] {
|
||
color: var(--md-sys-color-on-secondary-container);
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] [data-navigation-pill] {
|
||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||
}
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- the navigation rail */
|
||
|
||
[data-navigation-rail] {
|
||
--navigation-rail-expanded-width: clamp(13.75rem, var(--navigation-rail-width, 16rem), 22.5rem);
|
||
--navigation-rail-collapsed-width: 6rem;
|
||
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);
|
||
|
||
@variant rail-collapsed {
|
||
width: var(--navigation-rail-collapsed-width);
|
||
}
|
||
}
|
||
|
||
/* M3's other collapsed width: NavigationRailCollapsedTokens.NarrowContainerWidth, 80dp against
|
||
the default 96. A variable rather than a width here, so the rules below — which each have
|
||
their own reason to name a collapsed rail's width — do not have to know about it. */
|
||
[data-navigation-rail][data-width='narrow'] {
|
||
--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. */
|
||
[data-navigation-rail][data-width='narrow'] [data-navigation-rail-item] [data-navigation-label] {
|
||
@variant rail-collapsed {
|
||
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 on a compact window. */
|
||
[data-navigation-rail='modal'] {
|
||
width: var(--navigation-rail-collapsed-width);
|
||
}
|
||
|
||
@media (width < 52.5rem) {
|
||
[data-navigation-rail='adaptive'] {
|
||
width: var(--navigation-rail-collapsed-width);
|
||
}
|
||
}
|
||
|
||
@media (width < 37.5rem) {
|
||
[data-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. `collapsed` and `expanded` mean what they say and are
|
||
left alone: wrap one in a `medium:` element if it must not show on a phone. */
|
||
[data-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-app-shell>'s `banner` slot holds, if the application made it sticky. The rail sticks
|
||
under it rather than behind it; 0 without one. */
|
||
[data-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));
|
||
/* Clip, not hide: no scroll container, so the destinations below can still scroll and
|
||
nothing sticky breaks. What only an expanded rail draws — a label, the brand — is drawn
|
||
at once when the rail expands, while the width is still growing; the clip keeps it
|
||
from spilling over the page for those frames. */
|
||
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". Neither applies to a rail open over a scrim, which is a surface over the page. */
|
||
[data-navigation-rail][data-divider]:not([data-open]) > [data-navigation-rail-panel] {
|
||
border-inline-end: 1px solid var(--md-sys-color-outline-variant);
|
||
}
|
||
|
||
[data-navigation-rail][data-fill='false']:not([data-open]) > [data-navigation-rail-panel] {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* Open: expanded over a scrim, in surface-container with a large corner at its inner edge. */
|
||
[data-navigation-rail][data-open] > [data-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 - 3.5rem);
|
||
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 < 37.5rem) {
|
||
[data-navigation-rail='adaptive'] > [data-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 - 3.5rem);
|
||
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),
|
||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||
|
||
&:dir(rtl) {
|
||
translate: 100% 0;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail='adaptive'][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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 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;
|
||
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),
|
||
display var(--md-sys-motion-effects-default-duration) allow-discrete;
|
||
}
|
||
|
||
[data-navigation-rail][data-open] > [data-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-navigation-rail-header] {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 0.5rem;
|
||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 2.75rem);
|
||
padding-bottom: 2rem;
|
||
}
|
||
|
||
[data-navigation-rail-panel] > [data-navigation-rail-destinations]:first-child {
|
||
padding-top: calc(var(--material-safe-top, env(safe-area-inset-top)) + 2.75rem);
|
||
}
|
||
|
||
[data-navigation-rail-destinations] {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
padding-block: 0.5rem;
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
[data-navigation-rail-destinations],
|
||
[data-navigation-rail-section],
|
||
[data-navigation-rail-footer] {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
@variant rail-collapsed {
|
||
gap: 0.25rem;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-footer] {
|
||
flex-shrink: 0;
|
||
padding-block: 0.5rem 1rem;
|
||
}
|
||
|
||
/* "Alignment: top or center; on tablets prefer center alignment for reach. Menu icon and FAB
|
||
are always top-aligned" (docs/reference/m3/components-navigation-selection-inputs.md
|
||
§ Navigation rail). Only the destinations move, then — the header and the footer stay at
|
||
their ends of the rail — 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-navigation-rail][data-align='center'] [data-navigation-rail-destinations] {
|
||
justify-content: safe center;
|
||
}
|
||
|
||
[data-navigation-rail-heading] {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 3rem;
|
||
padding-inline: 2.25rem 1.25rem;
|
||
overflow: hidden;
|
||
color: var(--md-sys-color-on-surface-variant);
|
||
font: var(--md-sys-typescale-title-sm);
|
||
letter-spacing: var(--md-sys-typescale-title-sm-tracking);
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
|
||
@variant rail-collapsed {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-section]:not(:first-child) {
|
||
@variant rail-collapsed {
|
||
margin-top: 0.75rem;
|
||
}
|
||
}
|
||
|
||
/* Expanded: a 56px full-width pill, icon and label 8px apart, label-large. */
|
||
[data-navigation-rail-item] {
|
||
--navigation-layer: 0;
|
||
position: relative;
|
||
isolation: isolate;
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
height: 3.5rem;
|
||
margin-inline: 1.25rem;
|
||
padding-inline: 1rem;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
color: var(--md-sys-color-on-surface-variant);
|
||
font: var(--md-sys-typescale-label-lg);
|
||
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
||
text-align: start;
|
||
text-decoration: none;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
outline: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
|
||
/* Collapsed: the icon in its 56×32 indicator over a label-medium label, 64px tall. */
|
||
@variant rail-collapsed {
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 0.25rem;
|
||
height: auto;
|
||
min-height: 4rem;
|
||
margin-inline: 0;
|
||
padding: 0.375rem 0.25rem;
|
||
border-radius: 0;
|
||
font: var(--md-sys-typescale-label-md);
|
||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||
white-space: normal;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item] [data-navigation-label] {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
|
||
@variant rail-collapsed {
|
||
flex: none;
|
||
display: -webkit-box;
|
||
max-width: 100%;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
text-align: center;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item][data-active] {
|
||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||
color: var(--md-sys-color-on-secondary-container);
|
||
|
||
@variant rail-collapsed {
|
||
background-image: none;
|
||
color: var(--md-sys-color-secondary);
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||
position: relative;
|
||
isolation: isolate;
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
|
||
@variant rail-collapsed {
|
||
width: 3.5rem;
|
||
height: 2rem;
|
||
}
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- both: indicator, states */
|
||
|
||
[data-navigation-bar-item] [data-navigation-indicator] {
|
||
position: relative;
|
||
isolation: isolate;
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 3.5rem;
|
||
height: 2rem;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
}
|
||
|
||
/* The vertical layout — a compact bar, and a tall one at any width — fills the 56×32 box
|
||
behind the icon; the horizontal layout fills the pill around icon and label instead, so it
|
||
takes the fill off the box again. */
|
||
[data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||
color: var(--md-sys-color-on-secondary-container);
|
||
}
|
||
|
||
@container (width >= 37.5rem) {
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item][data-active] [data-navigation-indicator] {
|
||
background-image: none;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item][data-active] [data-navigation-indicator] {
|
||
@variant rail-collapsed {
|
||
background-image: linear-gradient(var(--md-sys-color-secondary-container), var(--md-sys-color-secondary-container));
|
||
color: var(--md-sys-color-on-secondary-container);
|
||
}
|
||
}
|
||
|
||
/* The active indicator is a secondary-container fill painted as a background image, so it can
|
||
grow out of its centre without stretching the icon: when a page arrives through
|
||
wire:navigate, resources/js/navigation.js starts it at zero width for a moment, and it
|
||
springs open on the default spatial spring, as Compose's indicator does when the selection
|
||
changes (NavigationItem.kt). A full page load draws it at once; reduced motion zeroes it. */
|
||
[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill]),
|
||
[data-navigation-rail-item],
|
||
[data-navigation-rail-item] [data-navigation-indicator] {
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background-size: 100% 100%;
|
||
transition: background-size var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default);
|
||
}
|
||
|
||
/* The state layer covers only the indicator: the pill when the indicator holds the label
|
||
too, the 56×32 shape when it holds the icon alone. Hover only where a pointer can hover. */
|
||
@media (hover: hover) {
|
||
:is([data-navigation-bar-item], [data-navigation-rail-item]):hover {
|
||
--navigation-layer: 0.08;
|
||
}
|
||
}
|
||
|
||
:is([data-navigation-bar-item], [data-navigation-rail-item]):is(:focus-visible, :active) {
|
||
--navigation-layer: 0.1;
|
||
}
|
||
|
||
[data-navigation-bar-item] [data-navigation-indicator]::before,
|
||
[data-navigation-bar-item] [data-navigation-pill]::before,
|
||
[data-navigation-rail-item] [data-navigation-indicator]::before,
|
||
[data-navigation-rail-item]::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: -1;
|
||
border-radius: inherit;
|
||
background-color: var(--md-sys-color-on-surface);
|
||
opacity: var(--navigation-layer);
|
||
pointer-events: none;
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
/* A navigation item's state layer is on-secondary-container, active or not: the only
|
||
state-layer tokens M3 states for one are NavigationRailColorTokens', which give that colour
|
||
to all six. The rail follows them two blocks below; the bar follows them here, over its
|
||
secondary-container pill. */
|
||
[data-navigation-bar-item] :is([data-navigation-indicator], [data-navigation-pill])::before {
|
||
background-color: var(--md-sys-color-on-secondary-container);
|
||
}
|
||
|
||
[data-navigation-bar-item] [data-navigation-pill]::before {
|
||
display: none;
|
||
}
|
||
|
||
@container (width >= 37.5rem) {
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-pill]::before {
|
||
display: block;
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item] [data-navigation-indicator]::before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* The rail item's layer is on the item while expanded and on its indicator while collapsed. A
|
||
variant cannot follow a pseudo-element in a selector, so the item hands the layer on through
|
||
variables; each rule below outweighs its line in the list above. */
|
||
[data-navigation-rail-item] {
|
||
--navigation-item-layer: var(--navigation-layer);
|
||
--navigation-indicator-layer: 0;
|
||
|
||
@variant rail-collapsed {
|
||
--navigation-item-layer: 0;
|
||
--navigation-indicator-layer: var(--navigation-layer);
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item][data-navigation-rail-item]::before {
|
||
background-color: var(--md-sys-color-on-secondary-container);
|
||
opacity: var(--navigation-item-layer);
|
||
}
|
||
|
||
[data-navigation-rail-item][data-navigation-rail-item] [data-navigation-indicator]::before {
|
||
background-color: var(--md-sys-color-on-secondary-container);
|
||
opacity: var(--navigation-indicator-layer);
|
||
}
|
||
|
||
/* M3's focus indicator, 3px of secondary 2px out, around the same shape. */
|
||
[data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
@container (width >= 37.5rem) {
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item]:focus-visible [data-navigation-indicator] {
|
||
outline: none;
|
||
}
|
||
|
||
[data-navigation-bar]:not([data-tall]) [data-navigation-bar-item]:focus-visible [data-navigation-pill] {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item]:focus-visible {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
|
||
@variant rail-collapsed {
|
||
outline: none;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-item]:focus-visible [data-navigation-indicator] {
|
||
@variant rail-collapsed {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* A bar that has slid off the bottom of the window is no longer there to clear, so the offset
|
||
* everything pinned to the bottom reads drops to the bottom safe area and whatever the application
|
||
* has docked on the bar — a `fab` button, the snackbar and the page's own bottom padding all follow
|
||
* it down and come back up with the bar. Unlayered on purpose: <x-app-shell> publishes the variable
|
||
* with a utility, and a rule in any layer loses to one. */
|
||
[data-app-shell]:has([data-navigation-bar][data-hide-on-scroll][data-hidden]) {
|
||
--material-bottom-bar: calc(var(--material-safe-bottom, env(safe-area-inset-bottom)) + var(--material-bottom-extra, 0px));
|
||
}
|
||
|
||
/* A FAB in the rail's header rests at elevation 0, not the 3 a standalone FAB has
|
||
* (docs/reference/m3/components-navigation-selection-inputs.md § Navigation rail: "when nested
|
||
* within another component, such as the navigation rail, the FAB's resting elevation should be
|
||
* level 0"). Unlayered on purpose: the FAB draws its shadow with a utility, and a rule in any
|
||
* layer loses to a utility — the same reason toolbar.css gives for its vibrant recolouring. */
|
||
[data-navigation-rail-header] [data-fab],
|
||
[data-navigation-rail-header] [data-fab]:hover {
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* "Rail transitions from collapsed to expanded should animate the FAB into and out of an extended
|
||
* FAB" — 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 closed and the extended
|
||
* FAB's own minimum width off, `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. Unlayered for the
|
||
* same reason as the rule above: `gap-2` and `min-w-20` are utilities, and a layer would lose to
|
||
* them. The cap is there because a transition needs a length to travel to; a rail FAB's label is a
|
||
* word or two. */
|
||
[data-navigation-rail-header] [data-fab] > span {
|
||
max-width: 16rem;
|
||
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);
|
||
|
||
@variant rail-collapsed {
|
||
max-width: 0;
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
[data-navigation-rail-header] [data-fab] {
|
||
@variant rail-collapsed {
|
||
min-width: 0;
|
||
aspect-ratio: 1;
|
||
gap: 0;
|
||
}
|
||
}
|