/Documentation

Navigation rail

PublishedUpdated

A navigation rail holds three to seven top-level destinations in a column along the side of a medium or expanded window. In M3 Expressive it has two states: collapsed, a 96dp column with the labels under the icons, and expanded, a 220 to 360dp panel with the labels beside them, which replaces the navigation drawer. See the M3 navigation rail guidelines.

Usage

Each destination has an id, a label and an icon; active marks the current one. A badge is a count or a short text, and badgeLabel says what it counts.

Vanilla
import { createNavigationRail } from 'mtrl';

const rail = createNavigationRail({
  ariaLabel: 'Mail',
  items: [
    {
      id: 'inbox',
      label: 'Inbox',
      icon: inboxIcon,
      badge: 24,
      badgeLabel: '24 unread',
      active: true,
    },
    { id: 'outbox', label: 'Outbox', icon: outboxIcon },
    { id: 'favorites', label: 'Favorites', icon: starIcon },
  ],
});
document.body.append(rail.element);
Web Components
<m-navigation-rail value="inbox" aria-label="Mail">
  <m-navigation-rail-item value="inbox" badge="24" badge-label="24 unread">Inbox</m-navigation-rail-item>
  <m-navigation-rail-item value="outbox">Outbox</m-navigation-rail-item>
  <m-navigation-rail-item value="favorites">Favorites</m-navigation-rail-item>
</m-navigation-rail>

<script type="module">
  const navigationRail = document.querySelector('m-navigation-rail');
  navigationRail.querySelector('m-navigation-rail-item[value="inbox"]').setAttribute('icon', inboxIcon);
  navigationRail.querySelector('m-navigation-rail-item[value="outbox"]').setAttribute('icon', outboxIcon);
  navigationRail.querySelector('m-navigation-rail-item[value="favorites"]').setAttribute('icon', starIcon);
</script>
React
import { useState } from 'react';
import { NavigationRail, NavigationRailItem } from 'mtrl/react';
import { inboxIcon, outboxIcon, starIcon } from './app';

export function Example() {
  const [value, setValue] = useState("inbox");
  return (
    <NavigationRail value={value} onChange={(event) => setValue(event.detail.value)} ariaLabel="Mail">
      <NavigationRailItem value="inbox" icon={inboxIcon} badge="24" badgeLabel="24 unread">Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
      <NavigationRailItem value="favorites" icon={starIcon}>Favorites</NavigationRailItem>
    </NavigationRail>
  );
}
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { MNavigationRail, MNavigationRailItem } from 'mtrl/vue';
import { inboxIcon, outboxIcon, starIcon } from './app';

const value = ref("inbox");
</script>

<template>
  <MNavigationRail v-model="value" aria-label="Mail">
    <MNavigationRailItem value="inbox" :icon="inboxIcon" badge="24" badge-label="24 unread">Inbox</MNavigationRailItem>
    <MNavigationRailItem value="outbox" :icon="outboxIcon">Outbox</MNavigationRailItem>
    <MNavigationRailItem value="favorites" :icon="starIcon">Favorites</MNavigationRailItem>
  </MNavigationRail>
</template>
Svelte
<script lang="ts">
  import { NavigationRail, NavigationRailItem } from 'mtrl/svelte';
  import { inboxIcon, outboxIcon, starIcon } from './app';

  let value = $state("inbox");
</script>

<NavigationRail bind:value ariaLabel="Mail">
  <NavigationRailItem value="inbox" icon={inboxIcon} badge="24" badgeLabel="24 unread">Inbox</NavigationRailItem>
  <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
  <NavigationRailItem value="favorites" icon={starIcon}>Favorites</NavigationRailItem>
</NavigationRail>
SolidJS
import { createSignal } from 'solid-js';
import { NavigationRail, NavigationRailItem } from 'mtrl/solid';
import { inboxIcon, outboxIcon, starIcon } from './app';

export function Example() {
  const [value, setValue] = createSignal("inbox");
  return (
    <NavigationRail value={value()} onChange={(event) => setValue(event.detail.value)} ariaLabel="Mail">
      <NavigationRailItem value="inbox" icon={inboxIcon} badge="24" badgeLabel="24 unread">Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
      <NavigationRailItem value="favorites" icon={starIcon}>Favorites</NavigationRailItem>
    </NavigationRail>
  );
}

Examples

Expanded

The menu button at the top expands and collapses the rail; expanded sets the state it starts in, and expandedWidth its expanded width. A destination with href is a link, and keeps the browser's navigation.

Vanilla
import { createNavigationRail } from 'mtrl';

const rail = createNavigationRail({
  expanded: true,
  expandedWidth: 320,
  items: [
    { id: 'inbox', label: 'Inbox', icon: inboxIcon, active: true },
    { id: 'outbox', label: 'Outbox', icon: outboxIcon, href: '/outbox' },
  ],
});
document.body.append(rail.element);
Web Components
<m-navigation-rail value="inbox" expanded expanded-width="320">
  <m-navigation-rail-item value="inbox">Inbox</m-navigation-rail-item>
  <m-navigation-rail-item value="outbox" href="/outbox">Outbox</m-navigation-rail-item>
</m-navigation-rail>

<script type="module">
  const navigationRail = document.querySelector('m-navigation-rail');
  navigationRail.querySelector('m-navigation-rail-item[value="inbox"]').setAttribute('icon', inboxIcon);
  navigationRail.querySelector('m-navigation-rail-item[value="outbox"]').setAttribute('icon', outboxIcon);
</script>
React
import { useState } from 'react';
import { NavigationRail, NavigationRailItem } from 'mtrl/react';
import { inboxIcon, outboxIcon } from './app';

export function Example() {
  const [value, setValue] = useState("inbox");
  return (
    <NavigationRail value={value} onChange={(event) => setValue(event.detail.value)} expanded expandedWidth={320}>
      <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon} href="/outbox">Outbox</NavigationRailItem>
    </NavigationRail>
  );
}
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { MNavigationRail, MNavigationRailItem } from 'mtrl/vue';
import { inboxIcon, outboxIcon } from './app';

const value = ref("inbox");
</script>

<template>
  <MNavigationRail v-model="value" expanded :expanded-width="320">
    <MNavigationRailItem value="inbox" :icon="inboxIcon">Inbox</MNavigationRailItem>
    <MNavigationRailItem value="outbox" :icon="outboxIcon" href="/outbox">Outbox</MNavigationRailItem>
  </MNavigationRail>
</template>
Svelte
<script lang="ts">
  import { NavigationRail, NavigationRailItem } from 'mtrl/svelte';
  import { inboxIcon, outboxIcon } from './app';

  let value = $state("inbox");
</script>

<NavigationRail bind:value expanded expandedWidth={320}>
  <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
  <NavigationRailItem value="outbox" icon={outboxIcon} href="/outbox">Outbox</NavigationRailItem>
</NavigationRail>
SolidJS
import { createSignal } from 'solid-js';
import { NavigationRail, NavigationRailItem } from 'mtrl/solid';
import { inboxIcon, outboxIcon } from './app';

export function Example() {
  const [value, setValue] = createSignal("inbox");
  return (
    <NavigationRail value={value()} onChange={(event) => setValue(event.detail.value)} expanded expandedWidth={320}>
      <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon} href="/outbox">Outbox</NavigationRailItem>
    </NavigationRail>
  );
}

With layout: 'modal', the collapsed rail is hidden and the expanded one opens over the page in a native modal dialog; Escape and the scrim collapse it. hideWhenCollapsed hides a standard rail while it is collapsed.

Vanilla
import { createNavigationRail } from 'mtrl';

const rail = createNavigationRail({
  layout: 'modal',
  items: [
    { id: 'inbox', label: 'Inbox', icon: inboxIcon, active: true },
    { id: 'outbox', label: 'Outbox', icon: outboxIcon },
  ],
});
document.body.append(rail.element);
Web Components
<m-navigation-rail value="inbox" layout="modal">
  <m-navigation-rail-item value="inbox">Inbox</m-navigation-rail-item>
  <m-navigation-rail-item value="outbox">Outbox</m-navigation-rail-item>
</m-navigation-rail>

<script type="module">
  const navigationRail = document.querySelector('m-navigation-rail');
  navigationRail.querySelector('m-navigation-rail-item[value="inbox"]').setAttribute('icon', inboxIcon);
  navigationRail.querySelector('m-navigation-rail-item[value="outbox"]').setAttribute('icon', outboxIcon);
</script>
React
import { useState } from 'react';
import { NavigationRail, NavigationRailItem } from 'mtrl/react';
import { inboxIcon, outboxIcon } from './app';

export function Example() {
  const [value, setValue] = useState("inbox");
  return (
    <NavigationRail value={value} onChange={(event) => setValue(event.detail.value)} layout="modal">
      <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
    </NavigationRail>
  );
}
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { MNavigationRail, MNavigationRailItem } from 'mtrl/vue';
import { inboxIcon, outboxIcon } from './app';

const value = ref("inbox");
</script>

<template>
  <MNavigationRail v-model="value" layout="modal">
    <MNavigationRailItem value="inbox" :icon="inboxIcon">Inbox</MNavigationRailItem>
    <MNavigationRailItem value="outbox" :icon="outboxIcon">Outbox</MNavigationRailItem>
  </MNavigationRail>
</template>
Svelte
<script lang="ts">
  import { NavigationRail, NavigationRailItem } from 'mtrl/svelte';
  import { inboxIcon, outboxIcon } from './app';

  let value = $state("inbox");
</script>

<NavigationRail bind:value layout="modal">
  <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
  <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
</NavigationRail>
SolidJS
import { createSignal } from 'solid-js';
import { NavigationRail, NavigationRailItem } from 'mtrl/solid';
import { inboxIcon, outboxIcon } from './app';

export function Example() {
  const [value, setValue] = createSignal("inbox");
  return (
    <NavigationRail value={value()} onChange={(event) => setValue(event.detail.value)} layout="modal">
      <NavigationRailItem value="inbox" icon={inboxIcon}>Inbox</NavigationRailItem>
      <NavigationRailItem value="outbox" icon={outboxIcon}>Outbox</NavigationRailItem>
    </NavigationRail>
  );
}

A selection emits select with { id, index, originalEvent }; originalEvent.preventDefault() keeps a link's navigation to the app's router. The web component dispatches change with { value }, the destination's id. header puts an element of the app's, such as a FAB, under the menu button; the web component takes it in its header slot.

API

Options

Option Type Default Description
items NavigationRailItemConfig[] [] Destinations; each needs a unique id, a label and an icon
expanded boolean false Whether it starts expanded
layout 'standard' | 'modal' 'standard' Standard rails take layout width; modal rails expand over the page in a native dialog
hideWhenCollapsed boolean false Hide the standard rail when collapsed (modal rails always do)
expandedWidth number 280 Expanded width in pixels, clamped to 220 to 360
showToggle boolean true Show the menu button that expands and collapses the rail
expandIcon string Material Symbols menu Menu button icon while collapsed
collapseIcon string Material Symbols menu_open Menu button icon while expanded
expandLabel string 'Expand navigation' Accessible name of the menu button while collapsed
collapseLabel string 'Collapse navigation' Accessible name of the menu button while expanded
header HTMLElement undefined Application-owned element under the menu button, such as a FAB
ripple boolean true Press ripple, clipped to the active indicator
ariaLabel string 'Primary navigation' Accessible name of the rail
class string undefined Additional CSS classes
onSelect (event) => void undefined Called with { id, index, originalEvent } when a destination is selected
onExpand / onCollapse () => void undefined Called when the rail expands or collapses

Items

Field Type Description
id string Unique identifier
label string Label text
icon string Icon markup
activeIcon string Icon markup while active, for a filled variant
href string Renders the destination as a link
badge string | number | boolean A large badge with text, or true for a dot
badgeLabel string Accessible description of the badge
active boolean Initially active
disabled boolean Not selectable, and out of the tab order

Methods

Method Returns Description
expand() / collapse() / toggle() NavigationRailComponent Expansion state
isExpanded() boolean Whether it is expanded
setActive(id) / getActive() NavigationRailComponent / string | null The active destination; setActive() emits nothing
setItems(items) / getItems() NavigationRailComponent / NavigationRailItemConfig[] Replace or read the destinations
setBadge(id, badge, label?) NavigationRailComponent Update a badge
on(event, handler) / off(event, handler) NavigationRailComponent Events
destroy() void Remove listeners and the element

Events

Event Payload Description
select { id, value, index, originalEvent } A destination was clicked or activated from the keyboard
expand / collapse { expanded } The rail expanded or collapsed

Accessibility

  • A nav landmark (a dialog in the modal layout) named by ariaLabel; the active destination has aria-current="page".
  • A badge is in its destination's name: the label, then badgeLabel, the badge's text, or "New activity" for a dot.
  • Tab reaches the destinations; Up, Down, Home and End move between the enabled ones.
  • The menu button has aria-expanded, and is named by expandLabel or collapseLabel. The modal rail keeps focus inside while it is open.

Styling

Selecting a destination grows the secondary-container indicator out of the middle of the item on the spatial spring. Expanding glides the width, the items and the indicator on the same spring, and the label moves beside the icon at the midpoint behind a fade. None of it runs with reduced motion.

.mtrl-navigation-rail { }                                   /* the rail */
.mtrl-navigation-rail--expanded, .mtrl-navigation-rail--modal { }
.mtrl-navigation-rail__toggle { }                           /* the menu button */
.mtrl-navigation-rail__item, .mtrl-navigation-rail__item--active { }
.mtrl-navigation-rail__indicator { }                        /* the active indicator */
.mtrl-navigation-rail__badge { }                            /* --dot for the dot */

Measurements

From the M3 Expressive rail tokens (Android navigationrail, version 34.0.0):

Attribute Collapsed Expanded
Width 96dp 220–360dp, 280dp by default
Destination 64dp tall, the label under the icon 56dp tall, the label beside the icon
Active indicator A 56 × 32dp pill 56dp tall, the item's width less 40dp
Icon 24dp 24dp
Menu button 48dp, 40dp above the destinations 48dp