Skip to content

Navigation

Navigation rail

Navigation at the side of medium and larger windows: a collapsed rail that expands, or opens as a modal.

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.

React HTML
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.

React HTML
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.

React HTML
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 }) from expand() / 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).