Navigation
Navigation rail
Navigation at the side of medium and larger windows: a collapsed rail that expands, or opens as a modal.
- COMPONENT_SPEC §14
- CSS · custom elements · React
- Open in the playground
Examples
Collapsed and expanded
The menu button expands the rail on Compose's springs: the width, the items and the labels move together.
The live demo runs with JavaScript. Its code is below.
import { useState } from 'react';
import {
Icon,
IconButton,
type NavigationDestination,
WideNavigationRail,
} from '@kyralabs/m3exp-web/react';
const ITEMS: NavigationDestination[] = [
{ key: 'home', icon: 'home', label: 'Home' },
{ key: 'search', icon: 'search', label: 'Search' },
{ key: 'mail', icon: 'mail', label: 'Mail', badge: 3 },
{ key: 'settings', icon: 'settings', label: 'Settings' },
];
export default function Wide() {
const [expanded, setExpanded] = useState(false);
return (
<div style={{ display: 'flex', blockSize: 'var(--m3e-sys-window-height-medium)' }}>
<WideNavigationRail
aria-label="Main, wide"
isExpanded={expanded}
items={ITEMS}
defaultSelectedKey="home"
header={
<IconButton
aria-label={expanded ? 'Collapse navigation' : 'Expand navigation'}
aria-expanded={expanded}
onPress={() => {
setExpanded((e) => !e);
}}
>
<Icon name={expanded ? 'menu_open' : 'menu'} />
</IconButton>
}
/>
</div>
);
}
<div style="display: flex; block-size: var(--m3e-sys-window-height-medium)">
<m3e-wide-navigation-rail id="rail" label="Main, wide" selected="home">
<button
type="button"
class="m3e-icon-button m3e-icon-button--standard"
aria-label="Expand navigation"
aria-expanded="false"
>
<span class="m3e-icon" aria-hidden="true">menu</span>
</button>
<m3e-navigation-item value="home" icon="home">Home</m3e-navigation-item>
<m3e-navigation-item value="search" icon="search">Search</m3e-navigation-item>
<m3e-navigation-item value="mail" icon="mail" badge="3">Mail</m3e-navigation-item>
<m3e-navigation-item value="settings" icon="settings">Settings</m3e-navigation-item>
</m3e-wide-navigation-rail>
</div>
<script>
const rail = document.getElementById('rail');
const menu = rail.querySelector('button');
menu.addEventListener('click', () => rail.toggle());
rail.addEventListener('expanded-change', (event) => {
if (event.target !== rail) return; // state events bubble in 1.x (D25)
const { expanded } = event.detail;
menu.setAttribute('aria-expanded', String(expanded));
menu.setAttribute('aria-label', expanded ? 'Collapse navigation' : 'Expand navigation');
menu.querySelector('.m3e-icon').textContent = expanded ? 'menu_open' : 'menu';
});
</script>
Run the HTML version (custom elements, no framework).
Modal
Collapsed it is a rail. Expanded, it opens as a modal window above a scrim: focus stays inside, and Escape or the scrim close it and return focus to the button.
The live demo runs with JavaScript. Its code is below.
import { useState } from 'react';
import {
Icon,
IconButton,
ModalWideNavigationRail,
type NavigationDestination,
} from '@kyralabs/m3exp-web/react';
const ITEMS: NavigationDestination[] = [
{ key: 'home', icon: 'home', label: 'Home' },
{ key: 'search', icon: 'search', label: 'Search' },
{ key: 'mail', icon: 'mail', label: 'Mail' },
{ key: 'settings', icon: 'settings', label: 'Settings' },
];
/** Collapsed it is a rail; the menu button opens it as a modal window over the page. */
export default function Modal() {
const [expanded, setExpanded] = useState(false);
return (
<div style={{ display: 'flex', blockSize: 'var(--m3e-sys-window-height-medium)' }}>
<ModalWideNavigationRail
aria-label="Main, modal"
items={ITEMS}
defaultSelectedKey="home"
isExpanded={expanded}
onExpandedChange={setExpanded}
header={
<IconButton
aria-label={expanded ? 'Close navigation' : 'Open navigation'}
aria-expanded={expanded}
onPress={() => {
setExpanded((e) => !e);
}}
>
<Icon name={expanded ? 'menu_open' : 'menu'} />
</IconButton>
}
/>
</div>
);
}
<div style="display: flex; block-size: var(--m3e-sys-window-height-medium)">
<m3e-modal-wide-navigation-rail id="modal-rail" label="Main, modal" selected="home">
<button
type="button"
class="m3e-icon-button m3e-icon-button--standard"
aria-label="Open navigation"
aria-expanded="false"
>
<span class="m3e-icon" aria-hidden="true">menu</span>
</button>
<m3e-navigation-item value="home" icon="home">Home</m3e-navigation-item>
<m3e-navigation-item value="search" icon="search">Search</m3e-navigation-item>
<m3e-navigation-item value="mail" icon="mail">Mail</m3e-navigation-item>
<m3e-navigation-item value="settings" icon="settings">Settings</m3e-navigation-item>
</m3e-modal-wide-navigation-rail>
</div>
<script>
const rail = document.getElementById('modal-rail');
const menu = rail.querySelector('button');
menu.addEventListener('click', () => rail.toggle());
rail.addEventListener('expanded-change', (event) => {
if (event.target !== rail) return; // state events bubble in 1.x (D25)
const { expanded } = event.detail;
menu.setAttribute('aria-expanded', String(expanded));
menu.setAttribute('aria-label', expanded ? 'Close navigation' : 'Open navigation');
menu.querySelector('.m3e-icon').textContent = expanded ? 'menu_open' : 'menu';
});
</script>
Run the HTML version (custom elements, no framework).
Navigation rail (deprecated)
Deprecated (D4): the 80dp NavigationRail ships until 2.0. Use the wide navigation rail.
import { type NavigationDestination, NavigationRail } from '@kyralabs/m3exp-web/react';
const ITEMS: NavigationDestination[] = [
{ key: 'home', icon: 'home', label: 'Home' },
{ key: 'search', icon: 'search', label: 'Search' },
{ key: 'settings', icon: 'settings', label: 'Settings' },
];
/** Deprecated (D4): use WideNavigationRail. */
export default function LegacyNavigationRail() {
return (
<div style={{ display: 'flex', blockSize: 'var(--m3e-sys-window-height-medium)' }}>
<NavigationRail aria-label="Main, legacy" items={ITEMS} defaultSelectedKey="home" />
</div>
);
}
<!-- Deprecated (D4). The CSS classes only lay the rail out: selection and focus are yours
(m3e-react's <NavigationRail> handles them). -->
<div style="display: flex; block-size: var(--m3e-sys-window-height-medium)">
<nav class="m3e-navigation-rail" aria-label="Main, legacy">
<button type="button" class="m3e-navigation-item" aria-current="page">
<span class="m3e-navigation-item__content">
<span class="m3e-navigation-item__indicator" aria-hidden="true"></span>
<span class="m3e-navigation-item__state" aria-hidden="true"></span>
<span class="m3e-icon m3e-navigation-item__icon" aria-hidden="true">home</span>
<span class="m3e-navigation-item__label">Home</span>
</span>
</button>
<button type="button" class="m3e-navigation-item">
<span class="m3e-navigation-item__content">
<span class="m3e-navigation-item__indicator" aria-hidden="true"></span>
<span class="m3e-navigation-item__state" aria-hidden="true"></span>
<span class="m3e-icon m3e-navigation-item__icon" aria-hidden="true">search</span>
<span class="m3e-navigation-item__label">Search</span>
</span>
</button>
<button type="button" class="m3e-navigation-item">
<span class="m3e-navigation-item__content">
<span class="m3e-navigation-item__indicator" aria-hidden="true"></span>
<span class="m3e-navigation-item__state" aria-hidden="true"></span>
<span class="m3e-icon m3e-navigation-item__icon" aria-hidden="true">settings</span>
<span class="m3e-navigation-item__label">Settings</span>
</span>
</button>
</nav>
</div>
Run the HTML version (CSS classes only).
API
React
<WideNavigationRail>
Wide navigation rail (COMPONENT_SPEC §14; Compose WideNavigationRail), collapsed or expanded. Switching isExpanded animates width, items and labels on Compose's springs with m3e-elements' WideNavigationRailLayout (one layout per frame, interruptible). For the modal rail, see <ModalWideNavigationRail>. Give it an aria-label.
| Prop | Type | Default | Description |
|---|---|---|---|
isExpanded
|
boolean
|
Expanded: icons before labels, the rail as wide as its widest item (220–360dp). Switching it animates the rail on Compose's springs (WideNavigationRailState.expand / collapse). | |
header
|
ReactNode
|
Content above the destinations, e.g. a menu button and a FAB. | |
selectedKey
|
string | null | undefined
|
The selected destination (controlled). Exactly one item gets aria-current="page".
|
|
defaultSelectedKey
|
string | null | undefined
|
The initially selected destination (uncontrolled). | |
items
required
|
readonly NavigationDestination[]
|
||
onSelectionChange
|
((key: string) => void) | undefined
|
Called when an item is pressed. |
Also accepts HTML attributes not listed here.
<ModalWideNavigationRail>
Modal wide navigation rail (COMPONENT_SPEC §14; Compose ModalWideNavigationRail). Collapsed it is a collapsed <WideNavigationRail>; expanded it opens as a modal window above a scrim (dialog semantics, focus trapped, Escape or the scrim collapse it, focus returns to the opener), animated on Compose's springs by m3e-elements' WideNavigationRailLayout, the code <m3e-modal-wide-navigation-rail> runs. aria-label names the <nav>; other props go to the root.
| Prop | Type | Default | Description |
|---|---|---|---|
isExpanded
|
boolean
|
Expanded: the rail opens as a modal window over the page (controlled). | |
defaultExpanded
|
boolean
|
The initial expanded state (uncontrolled). | |
hideOnCollapse
|
boolean
|
Show nothing while collapsed and slide the expanded rail in from the start edge (Compose
hideOnCollapse); by default the collapsed rail stays in the layout.
|
|
header
|
ReactNode
|
Content above the destinations, e.g. a menu button and a FAB. | |
dialogLabel
|
string
|
The modal window's accessible name (default "Navigation rail", Compose's pane title). | |
selectedKey
|
string | null | undefined
|
The selected destination (controlled). Exactly one item gets aria-current="page".
|
|
defaultSelectedKey
|
string | null | undefined
|
The initially selected destination (uncontrolled). | |
items
required
|
readonly NavigationDestination[]
|
||
onExpandedChange
|
(expanded: boolean) => void
|
Called when the rail asks to change state: Escape or a press on the scrim collapse it
(WideNavigationRailState.collapse()). Controlled rails stay open until isExpanded follows.
|
|
onSelectionChange
|
((key: string) => void) | undefined
|
Called when an item is pressed. |
Also accepts HTML attributes not listed here.
<NavigationRail>
Deprecated. D4: use WideNavigationRail. Shipped until 2.0; no new features.
Legacy navigation rail (Compose NavigationRail): 80dp wide, items centered.
| Prop | Type | Default | Description |
|---|---|---|---|
header
|
ReactNode
|
Content above the destinations, e.g. a FAB. | |
selectedKey
|
string | null | undefined
|
The selected destination (controlled). Exactly one item gets aria-current="page".
|
|
defaultSelectedKey
|
string | null | undefined
|
The initially selected destination (uncontrolled). | |
items
required
|
readonly NavigationDestination[]
|
||
onSelectionChange
|
((key: string) => void) | undefined
|
Called when an item is pressed. |
Also accepts HTML attributes not listed here.
Custom elements
<m3e-wide-navigation-rail>
import '@kyralabs/m3e-elements/wide-navigation-rail.js';
<m3e-wide-navigation-rail label="Main" selected="home"> (COMPONENT_SPEC §14; Compose WideNavigationRail). Renders the m3e-css .m3e-wide-navigation-rail contract inside itself (the host is display: contents): a <nav> of its <m3e-navigation-item> children; other children (a menu button, a FAB) go to the header. Switching expanded animates the rail on Compose's springs (m3e-elements WideNavigationRailLayout, shared with m3e-react).
- Attributes:
label,selected(see M3eNavigationContainer),expanded(reflected). - Events:
selection-change(detail: { value }, cancelable);expanded-change(detail: { expanded }) fromexpand()/collapse()/toggle().
| Attribute | Property | Type | Notes |
|---|---|---|---|
label
|
label
|
string
|
|
selected
|
selected
|
string
|
Reflected. |
expanded
|
expanded
|
boolean
|
Reflected. |
| Event | Detail | Description |
|---|---|---|
expanded-change
|
ExpandedChangeDetail
|
Expanded or collapsed; cancelable when the rail asks. |
<m3e-modal-wide-navigation-rail>
import '@kyralabs/m3e-elements/modal-wide-navigation-rail.js';
<m3e-modal-wide-navigation-rail label="Main"> (COMPONENT_SPEC §14; Compose ModalWideNavigationRail). Renders the .m3e-modal-wide-navigation-rail contract: the rail (as <m3e-wide-navigation-rail>) and the scrim. Collapsed it is a collapsed rail; expanded opens it as a modal window (dialog semantics, focus trapped, Escape or the scrim collapse it, focus returns to the opener) over the page; hide-on-collapse shows nothing while collapsed and slides the expanded rail in.
- Attributes:
label,selected,expanded(reflected),hide-on-collapse,dialog-label(the window's accessible name, default "Navigation rail", Compose Strings.WideNavigationRailPaneTitle). - Events:
selection-change;expanded-change(detail: { expanded }) from the methods and when Escape or the scrim collapse it (cancelable:preventDefault()keeps it open).
| Attribute | Property | Type | Notes |
|---|---|---|---|
label
|
label
|
string
|
|
selected
|
selected
|
string
|
Reflected. |
expanded
|
expanded
|
boolean
|
Reflected. |
hide-on-collapse
|
hideOnCollapse
|
boolean
|
|
dialog-label
|
dialogLabel
|
string
|
| Event | Detail | Description |
|---|---|---|
expanded-change
|
ExpandedChangeDetail
|
Expanded or collapsed; cancelable when the rail asks. |
CSS
wide-navigation-rail.css
@import '@kyralabs/m3e-css/wide-navigation-rail.css';
Wide navigation rail (COMPONENT_SPEC §14; Compose WideNavigationRail / ModalWideNavigationRail), collapsed, expanded and animated between the two, plus the modal rail.
<nav class="m3e-wide-navigation-rail" aria-label="Main">
<div class="m3e-wide-navigation-rail__header">…menu button, FAB…</div> (optional)
<a class="m3e-navigation-item" href="/" aria-current="page">…</a> (navigation-item.css)
…
</nav>
<div class="m3e-modal-wide-navigation-rail"> (modal)
<nav class="m3e-wide-navigation-rail" aria-label="Main">…</nav>
<div class="m3e-modal-wide-navigation-rail__scrim" aria-hidden="true"></div>
</div>
Collapsed (default): icons above labels, 96dp. --expanded: icons before labels, the rail as wide as its widest item within the expanded min/max widths. Static rails need no JavaScript. Every item position is one formula of the progress Compose animates, so the same CSS draws the collapsed rail (progress 0), the expanded rail (1) and every frame between them: m3e-elements' WideNavigationRailLayout (also run by m3e-react) only writes, once per frame,
--_m3e-wnr-progress the items' iconPositionProgress (DefaultSpatial),
--_m3e-wnr-layout itemVerticalSpacedBy / itemMinHeight (DefaultSpatial),
--_m3e-wnr-inline-size the rail width WideNavigationRailLayout measures (m3e-motion),
plus [data-animating] while they move: no layout is read during the animation, so the browser lays the rail out once per frame. The modal root also gets --_m3e-wnr-scrim (alpha) and --_m3e-wnr-slide (hideOnCollapse) and [data-open] while its window is shown: the rail and the scrim then sit in the top layer (popover="manual"; fixed positioning without the Popover API), the root keeping the collapsed rail's place.
41 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-divider-thickness
|
1px |
--m3e-comp-minimum-interactive-component-size
|
48px |
--m3e-comp-modal-wide-navigation-rail-container-color
|
var(--m3e-sys-color-surface-container) |
--m3e-comp-modal-wide-navigation-rail-container-shape
|
var(--m3e-sys-shape-corner-large) |
--m3e-comp-modal-wide-navigation-rail-expanded-header-top-padding
|
0px |
--m3e-comp-modal-wide-navigation-rail-scrim-color
|
var(--m3e-sys-color-scrim) |
--m3e-comp-modal-wide-navigation-rail-scrim-opacity
|
0.32 |
--m3e-comp-navigation-rail-baseline-item-container-height
|
64px |
--m3e-comp-navigation-rail-baseline-item-header-space-minimum
|
40px |
--m3e-comp-navigation-rail-baseline-item-icon-size
|
24px |
--m3e-comp-navigation-rail-collapsed-container-color
|
var(--m3e-sys-color-surface) |
--m3e-comp-navigation-rail-collapsed-container-shape
|
var(--m3e-sys-shape-corner-none) |
--m3e-comp-navigation-rail-collapsed-container-width
|
96px |
--m3e-comp-navigation-rail-collapsed-item-vertical-space
|
4px |
--m3e-comp-navigation-rail-collapsed-top-space
|
44px |
--m3e-comp-navigation-rail-color-item-active-icon
|
var(--m3e-sys-color-on-secondary-container) |
--m3e-comp-navigation-rail-color-item-active-indicator
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-navigation-rail-color-item-active-label-text
|
var(--m3e-sys-color-secondary) |
--m3e-comp-navigation-rail-color-item-inactive-icon
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-navigation-rail-color-item-inactive-label-text
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-navigation-rail-expanded-container-shape
|
var(--m3e-sys-shape-corner-none) |
--m3e-comp-navigation-rail-expanded-container-width-maximum
|
360px |
--m3e-comp-navigation-rail-expanded-container-width-minimum
|
220px |
--m3e-comp-navigation-rail-horizontal-item-active-indicator-height
|
56px |
--m3e-comp-navigation-rail-horizontal-item-icon-label-space
|
8px |
--m3e-comp-navigation-rail-horizontal-item-label-text-font
|
var(--m3e-sys-typescale-label-large-font) |
--m3e-comp-navigation-rail-horizontal-item-label-text-line-height
|
var(--m3e-sys-typescale-label-large-line-height) |
--m3e-comp-navigation-rail-horizontal-item-label-text-size
|
var(--m3e-sys-typescale-label-large-size) |
--m3e-comp-navigation-rail-horizontal-item-label-text-tracking
|
var(--m3e-sys-typescale-label-large-tracking) |
--m3e-comp-navigation-rail-horizontal-item-label-text-weight
|
var(--m3e-sys-typescale-label-large-weight) |
--m3e-comp-navigation-rail-vertical-item-active-indicator-height
|
32px |
--m3e-comp-navigation-rail-vertical-item-active-indicator-width
|
56px |
--m3e-comp-navigation-rail-vertical-item-icon-label-space
|
4px |
--m3e-comp-navigation-rail-vertical-item-label-text-font
|
var(--m3e-sys-typescale-label-medium-font) |
--m3e-comp-navigation-rail-vertical-item-label-text-line-height
|
var(--m3e-sys-typescale-label-medium-line-height) |
--m3e-comp-navigation-rail-vertical-item-label-text-size
|
var(--m3e-sys-typescale-label-medium-size) |
--m3e-comp-navigation-rail-vertical-item-label-text-tracking
|
var(--m3e-sys-typescale-label-medium-tracking) |
--m3e-comp-navigation-rail-vertical-item-label-text-weight
|
var(--m3e-sys-typescale-label-medium-weight) |
--m3e-comp-wide-navigation-rail-item-horizontal-padding
|
20px |
--m3e-comp-wide-navigation-rail-label-color-duration
|
var(--m3e-sys-motion-spring-default-effects-duration) |
--m3e-comp-wide-navigation-rail-label-color-easing
|
var(--m3e-sys-motion-spring-default-effects-easing) |
navigation-rail.css
@import '@kyralabs/m3e-css/navigation-rail.css';
@deprecated (D4) Legacy navigation rail (COMPONENT_SPEC §14; Compose NavigationRail). Use the wide navigation rail (wide-navigation-rail.css). Shipped until 2.0; no new features.
<nav class="m3e-navigation-rail" aria-label="Main">
<div class="m3e-navigation-rail__header">…FAB…</div> (optional)
<a class="m3e-navigation-item" href="/" aria-current="page">…</a> (navigation-item.css)
…
</nav>
18 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-divider-thickness
|
1px |
--m3e-comp-navigation-rail-baseline-item-icon-size
|
24px |
--m3e-comp-navigation-rail-collapsed-container-color
|
var(--m3e-sys-color-surface) |
--m3e-comp-navigation-rail-collapsed-narrow-container-width
|
80px |
--m3e-comp-navigation-rail-color-item-active-icon
|
var(--m3e-sys-color-on-secondary-container) |
--m3e-comp-navigation-rail-color-item-active-indicator
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-navigation-rail-color-item-active-label-text
|
var(--m3e-sys-color-secondary) |
--m3e-comp-navigation-rail-color-item-inactive-icon
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-navigation-rail-color-item-inactive-label-text
|
var(--m3e-sys-color-on-surface-variant) |
--m3e-comp-navigation-rail-header-padding
|
8px |
--m3e-comp-navigation-rail-item-vertical-padding
|
4px |
--m3e-comp-navigation-rail-vertical-item-active-indicator-height
|
32px |
--m3e-comp-navigation-rail-vertical-item-active-indicator-width
|
56px |
--m3e-comp-navigation-rail-vertical-item-label-text-font
|
var(--m3e-sys-typescale-label-medium-font) |
--m3e-comp-navigation-rail-vertical-item-label-text-size
|
var(--m3e-sys-typescale-label-medium-size) |
--m3e-comp-navigation-rail-vertical-item-label-text-tracking
|
var(--m3e-sys-typescale-label-medium-tracking) |
--m3e-comp-navigation-rail-vertical-item-label-text-weight
|
var(--m3e-sys-typescale-label-medium-weight) |
--m3e-comp-navigation-rail-vertical-padding
|
4px |
Accessibility
<nav aria-label>; items aria-current="page"; modal window role="dialog" aria-modal="true" with a focus trap; the scrim is aria-hidden (Escape and the header button collapse it from the keyboard).
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Differences from Compose
labels are one line (ellipsized past the collapsed rail width or the expanded maximum; Compose wraps them and grows the item); the item hit area during an animation is its natural width (Compose: the measured lerp); Escape / back in hideOnCollapse slide the rail out before the window closes (Compose's state.collapse() path closes the window when the 165ms positionProgress settles, mid-slide; its scrim path slides out first); no predictive back and no swipe-to-dismiss; the modal window rail is fixed at the viewport's start edge (Compose: the dialog window's); WideNavigationRailColors customization is by overriding the tokens.
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §14 and the m3e-css sources.
WideNavigationRail.kt, WideNavigationRailState.kt, NavigationItem.kt (vendored), androidMain WideNavigationRail.android.kt and res/values/strings.xml L139-142 (read: dialog window, pane title / close strings), Scrim.kt (read), NavigationRail*Tokens.kt, ScrimTokens.kt, NavigationRailSamples.kt (vendored: WideNavigationRailResponsiveSample, ModalWideNavigationRailSample, DismissibleModalWideNavigationRailSample).
NavigationRail{Collapsed,Expanded,Color,BaselineItem,VerticalItem,HorizontalItem}Tokens.kt@19991f2c: collapsed 96dp, expanded 220–360dp, top space 44dp, header → items 40dp, collapsed item min height 64dp spaced 4dp; pill 56×32 collapsed, 56dp tall expanded starting WNRItemHorizontalPadding (20dp, generated from WideNavigationRail.kt) from the start edge. Header/FAB placement: header slot at the top start (WideNavigationRailLayout). Legacy NavigationRail: 80dp, items 56dp, literals from NavigationRail.kt (generated). M7 adds to components/navigation-defaults.css (PARITY.md lines):
wide-navigation-rail.css sources
(compose/material3 @ 19991f2c):
- WideNavigationRail.kt (WideNavigationRailLayout L301-527): collapsed width NavigationRailCollapsedTokens.ContainerWidth; expanded width between NavigationRailExpandedTokens.ContainerWidthMinimum and ContainerWidthMaximum, at least the widest item + WNRItemHorizontalPadding; top padding NavigationRailCollapsedTokens.TopSpace (WideNavigationRailDefaults.ContentPadding); header then HeaderSpaceMinimum before the items; items spaced NavigationRailCollapsedTokens.ItemVerticalSpace → 0 and at least NavigationRailBaselineItemTokens.ContainerHeight → the minimum touch target tall (both on DefaultSpatial). Container NavigationRailCollapsedTokens.ContainerColor and ContainerShape (both states when not modal), clipped to it (Surface); window insets vertical + start.
- NavigationItem.kt (AnimatedNavigationItem L440-554, AnimatedMeasurePolicy L857-944, placeAnimatedLabelAndIcon L1103-1199) for WideNavigationRailItem: with p = iconPositionProgress (≥ 0), the pill starts WNRItemHorizontalPadding from the item's start, is lerp(icon, icon + gap + label, p) + padding wide and lerp(icon, max(icon, label), p) + padding tall, the padding (WideNavigationRailItemDefaults.indicatorPadding) going from IndicatorCollapsedPadding ((ActiveIndicatorWidth − IconSize) / 2, (ActiveIndicatorHeight − IconSize) / 2) to IndicatorExpandedPadding (FullWidthLeadingSpace, (NavigationRailHorizontalItemTokens.ActiveIndicatorHeight − IconSize) / 2); the item is lerp(heightTop, heightStart, p) tall; the icon moves down to the middle; the label fades out below the pill (alpha 4 (p − ½)²) and back in after the icon (x = (icon x + icon + gap − offset) · p), switching place at p = ½ and type style when collapsing past ½. The indicator grows from the pill's start (indicatorX = item padding); icon-only items keep a centered circle.
- Colors: WideNavigationRailItemDefaults (NavigationRailColorTokens; selected label ItemActiveLabelText with the icon on top, ItemActiveIcon at the start). The label color animates on DefaultEffects (StyledLabel animateColor), the indicator on DefaultSpatial.
- Modal (ModalWideNavigationRail L577-623, ModalWideNavigationRailImpl L705-864, ModalWideNavigationRailContent L1493-1621, WideNavigationRailDefaults L1229-1322): the window rail at the window's start edge, full height, modalContainerColor (NavigationRailExpandedTokens.ModalContainerColor) with modalExpandedShape (ModalContainerShape) and no elevation; the header gets expandedHeaderTopPadding; the scrim is ScrimTokens.ContainerColor at ContainerOpacity × its alpha; the collapsed rail stays in place under it. hideOnCollapse: no collapsed rail; the expanded rail slides in from −its width.
navigation-rail.css sources
(compose/material3 @ 19991f2c):
- NavigationRail.kt: container NavigationRailCollapsedTokens.ContainerColor, content contentColorFor (onSurface), min width NavigationRailCollapsedTokens.NarrowContainerWidth, vertical padding and item spacing NavigationRailVerticalPadding, header followed by a NavigationRailHeaderPadding spacer, everything centered horizontally; window insets vertical + start (here the safe-area insets).
- NavigationRailItem: min width NavigationRailItemWidth (NarrowContainerWidth), min height NavigationRailItemHeight (NavigationRailVerticalItemTokens.ActiveIndicatorWidth); indicator ActiveIndicatorWidth × ActiveIndicatorHeight with a label (NavigationRailItemVerticalPadding below), a CornerFull circle of ActiveIndicatorWidth without one; content centered with the indicator's vertical padding counted once (placeLabelAndIcon). Icon and label colors animate on DefaultEffects; the indicator width on FastSpatial, its alpha on DefaultEffects. Colors: NavigationRailItemDefaults (NavigationRailColorTokens).