Skip to content

Navigation

Navigation suite

One set of destinations shown as a navigation bar, a rail or a drawer, chosen by the window size class.

Examples

Adaptive navigation

One set of destinations, shown as the window size class calls for: a short navigation bar below 600px, a collapsed wide rail from 600px and the expanded rail from 1200px. The FAB floats above the bar and heads the rail. Switching keeps the selection and the focus.

The live demo runs with JavaScript. Its code is below.

React HTML
import { useState } from 'react';
import {
  Fab,
  Icon,
  NavigationSuiteScaffold,
  type NavigationSuiteDestination,
} from '@kyralabs/m3exp-web/react';

const ITEMS: NavigationSuiteDestination[] = [
  { key: 'home', icon: 'home', label: 'Home' },
  { key: 'search', icon: 'search', label: 'Search', badge: true },
  { key: 'mail', icon: 'mail', label: 'Mail', badge: 3 },
  { key: 'settings', icon: 'settings', label: 'Settings' },
];

/** A bar below 600px, a collapsed rail from 600px, the expanded rail from 1200px. */
export default function Adaptive() {
  const [selected, setSelected] = useState('home');
  return (
    <div
      style={{
        inlineSize: '100%',
        blockSize: 'var(--m3e-sys-window-height-medium)',
        overflow: 'hidden',
      }}
    >
      <NavigationSuiteScaffold
        aria-label="Main"
        items={ITEMS}
        selectedKey={selected}
        onSelectionChange={setSelected}
        primaryAction={<Fab icon={<Icon name="edit" />} aria-label="Compose" />}
      >
        <p style={{ padding: 'var(--m3e-sys-space-200)' }}>
          {ITEMS.find((item) => item.key === selected)?.label}: resize the window to switch between
          the bar and the rail.
        </p>
      </NavigationSuiteScaffold>
    </div>
  );
}
<div style="inline-size: 100%; block-size: var(--m3e-sys-window-height-medium); overflow: hidden">
  <m3e-navigation-suite label="Main" selected="home">
    <m3e-navigation-item value="home" icon="home">Home</m3e-navigation-item>
    <m3e-navigation-item value="search" icon="search" badge="">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>
    <button type="button" slot="primary-action" class="m3e-fab" aria-label="Compose">
      <span class="m3e-icon" aria-hidden="true">edit</span>
    </button>
    <p style="padding: var(--m3e-sys-space-200)">
      Resize the window to switch between the bar and the rail.
    </p>
  </m3e-navigation-suite>
</div>

Run the HTML version (custom elements, no framework).

API

React

<NavigationSuiteScaffold>

Navigation suite scaffold (COMPONENT_SPEC §16; Compose NavigationSuiteScaffold): one set of destinations shown as the navigation the window size class calls for — a short navigation bar below 600px (horizontal items at compact height), a collapsed wide navigation rail from 600px, the expanded rail from 1200px — beside or above the content. Switching keeps the selection, the item elements (bar ↔ rail; the same <nav>, as Compose's movableContentOf keeps its items) and keyboard focus. Show / hide animates with m3e-elements' NavigationSuiteLayout (the code <m3e-navigation-suite> runs). aria-label names the <nav>; other props go to the root.

Prop Type Default Description
layoutType "none" | "short-navigation-bar-compact" | "short-navigation-bar-medium" | "wide-navigation-rail-collapsed" | "wide-navigation-rail-expanded" | "navigation-drawer" A fixed navigation type (Compose's navigationSuiteType / layoutType argument), e.g. navigation-drawer; it wins over the adaptive rule. Default: adaptive.
navigationSuiteType ((sizeClass: WindowSizeClass) => NavigationSuiteType) | undefined The adaptive rule: a function of the window size class (useWindowSizeClass). Default: m3e-motion's navigationSuiteType(sizeClass, { expandedFrom, expandedType }).
expandedFrom NavigationSuiteTypeOptions['expandedFrom'] 'large' Where the default rule expands the navigation (a width class or px bound); null keeps Compose's own rule (bar / collapsed rail only). Default 'large' (m3.material.io).
expandedType NavigationSuiteTypeOptions['expandedType'] What the expanded navigation is: the expanded wide rail (default) or a drawer.
isNavigationHidden boolean | undefined Hides the navigation (NavigationSuiteScaffoldState); switching it animates on Compose's spring. The content takes the freed space.
primaryAction ReactNode Primary action (typically a FAB): above a bar, the header of a rail, the first entry of a drawer (Compose primaryActionContent).
primaryActionAlignment 'start' | 'center' | 'end' | undefined Where the primary action sits above a bar (Compose primaryActionContentHorizontalAlignment).
children ReactNode The content (put your <main> here when the suite frames the page).
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 NavigationSuiteDestination[]
onNavigationSuiteTypeChange ((type: NavigationSuiteType) => void) | undefined Called when the window size (or a prop) changes the navigation type.
onSelectionChange ((key: string) => void) | undefined Called when an item is pressed.

Also accepts HTML attributes not listed here.

Custom elements

<m3e-navigation-suite>

import '@kyralabs/m3e-elements/navigation-suite.js';

<m3e-navigation-suite label="Main" selected="home"> (COMPONENT_SPEC §16; Compose NavigationSuiteScaffold). Renders the m3e-css .m3e-navigation-suite contract inside itself (the host is display: contents): its <m3e-navigation-item> children as the navigation the window size class calls for — a short navigation bar below 600px (horizontal items at compact height), a collapsed wide navigation rail from 600px, the expanded rail from 1200px — and every other child in the content area. A child with slot="primary-action" (e.g. a FAB) floats above a bar and heads the rail or drawer. Switching keeps the selection, the item elements (bar ↔ rail) and keyboard focus. Show / hide animates on Compose's spring (m3e-elements NavigationSuiteLayout, shared with m3e-react).

  • Attributes: label (the <nav>'s name), selected (the selected item's value), layout-type (a fixed NavigationSuiteType: short-navigation-bar-compact, short-navigation-bar-medium, wide-navigation-rail-collapsed, wide-navigation-rail-expanded, navigation-drawer or none; default: adaptive), expanded-from (where the adaptive rule expands: a width class or px bound, none for Compose's own rule; default large), expanded-type (wide-navigation-rail-expanded default, or navigation-drawer), navigation-hidden (reflected; switching it animates), primary-action-alignment (start | center | end, the default: where the primary action sits above a bar).
  • Property navigationSuiteType: a function of the window size class that replaces the adaptive rule (layout-type still wins). Read-only type: the type shown.
  • Methods: showNavigation(), hideNavigation(), toggleNavigation() (NavigationSuiteScaffoldState.show / hide / toggle).
  • Events: selection-change (detail: { value }, cancelable); navigation-hidden-change (detail: { hidden }) from the methods; navigation-suite-type-change (detail: { type }) when the window size (or a property) changes the type.
Attribute Property Type Notes
label label string
selected selected string Reflected.
layout-type layoutType string
expanded-from expandedFrom string
expanded-type expandedType string
navigation-hidden navigationHidden boolean Reflected.
primary-action-alignment primaryActionAlignment string
Event Detail Description
navigation-hidden-change NavigationHiddenChangeDetail The navigation was shown or hidden.
navigation-suite-type-change NavigationSuiteTypeChangeDetail The navigation component changed with the window size class.

CSS

navigation-suite.css

@import '@kyralabs/m3e-css/navigation-suite.css';

Navigation suite scaffold (COMPONENT_SPEC §16; Compose NavigationSuiteScaffold / NavigationSuiteScaffoldLayout / NavigationSuite): one set of destinations shown as the navigation component for the window size, plus the content area.

<div class="m3e-navigation-suite m3e-navigation-suite--bar">        (ShortNavigationBar types)
  <nav class="m3e-short-navigation-bar" aria-label="Main">…</nav>    (short-navigation-bar.css;
                                                                       --horizontal: medium)
  <div class="m3e-navigation-suite__primary-action">…FAB…</div>     (optional)
  <div class="m3e-navigation-suite__content">…</div>
</div>

<div class="m3e-navigation-suite m3e-navigation-suite--rail">       (WideNavigationRail types)
  <nav class="m3e-wide-navigation-rail" aria-label="Main">           (wide-navigation-rail.css;
    <div class="m3e-wide-navigation-rail__header">…FAB…</div>          --expanded: expanded)
    …
  </nav>
  <div class="m3e-navigation-suite__content">…</div>
</div>

<div class="m3e-navigation-suite m3e-navigation-suite--drawer">     (NavigationDrawer type)
  <div class="m3e-navigation-drawer">                                (navigation-drawer.css)
    <nav class="m3e-navigation-drawer__sheet" aria-label="Main">
      <div class="m3e-navigation-suite__primary-action">…FAB…</div> (optional)
      …
    </nav>
  </div>
  <div class="m3e-navigation-suite__content">…</div>
</div>

--none: the content alone. The bar sits at the bottom (block end), the rail and the drawer at the inline start (right in RTL), the content fills the rest and scrolls on its own. The suite fills its container's height (give it one, e.g. 100dvh for a whole page). With a bar, the primary action floats above it at the end (--primary-action-start / --primary-action-center move it); with a rail it is the rail's header, with a drawer the first entry of the sheet (both aligned with the item indicators).

Sizing is the window's, as Compose's navigationSuiteType reads the window size class: the elements and React classify the viewport (m3e-elements observeWindowSizeClass, the same min-width queries as below) and render the matching contract. --adaptive is the no-JavaScript fallback: put every navigation it may need in the suite (a bar, a --horizontal bar, a rail, an --expanded rail or a drawer; one primary action wrapper for the bars) and the window size picks the one the scaffold's default rule picks: the bar below 600px wide, the --horizontal bar at 600px and wider when the window is under 480px tall, the collapsed rail from 600px, the expanded rail (or drawer) from 1200px. Media queries rather than container queries: the suite is the outermost frame of an app, and a container query could not restyle the suite element itself.

Show / hide (NavigationSuiteScaffoldState): --navigation-hidden is the resting hidden state (the navigation is out of the layout, invisible and not focusable; a bar's primary action stays). m3e-elements' NavigationSuiteLayout (also run by m3e-react) animates it: per frame it writes only

--_m3e-ns-offset   how far the navigation is pushed out (px): its height (bar) or width
                   (rail, drawer) − (size × progress).toInt(),

plus [data-animating], measuring the navigation with a ResizeObserver (no layout reads).

6 custom properties it reads
Custom property Default
--m3e-comp-navigation-drawer-item-horizontal-padding 12px
--m3e-comp-navigation-suite-scaffold-container-color var(--m3e-sys-color-background)
--m3e-comp-navigation-suite-scaffold-content-color var(--m3e-sys-color-on-background)
--m3e-comp-navigation-suite-scaffold-primary-action-alignment end
--m3e-comp-navigation-suite-scaffold-primary-action-padding 16px
--m3e-comp-wide-navigation-rail-item-horizontal-padding 20px

Accessibility

the navigation is a <nav> named by label / aria-label with exactly one aria-current="page" (items are links or buttons, §13–15); landmarks for the content are the consumer's (put <main> in the content when the suite frames the page; the docs page, which has its own <main>, does not).

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

no tabletop posture on the web (the default rule never sees it); type switches jump, as in Compose, rather than animating collapsed ↔ expanded; the primary action is aligned with the item indicators in the rail and drawer (Compose: unpadded); when the navigation hides with focus inside it, focus is not moved (it is lost to the document, as the navigation becomes hidden); badges only in the drawer type (Badge, §34).

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §16 and the m3e-css sources.

material3-adaptive-navigation-suite NavigationSuiteScaffold.kt and NavigationSuiteScaffoldSamples.kt (vendored, androidx-material3-adaptive), animation-core VectorizedAnimationSpec.kt (vendored), window-core WindowSizeClass.kt (vendored); §13–15 sources for the navigation components; m3.material.io navigation guidance via FOUNDATIONS §9 and the owner decision (expanded rail at large).

no NavigationSuite*Tokens exist; components/navigation-suite-defaults.css (PARITY.md lines):

The navigation components keep their own tokens (§13–15).

navigation-suite.css sources

(material3-adaptive-navigation-suite NavigationSuiteScaffold.kt @ 19991f2c; values in navigation-suite-defaults.css, lines in PARITY.md):

  • NavigationSuiteScaffold (L192-232): a Surface in NavigationSuiteScaffoldDefaults.containerColor (background) / contentColor (onBackground) around NavigationSuiteScaffoldLayout.
  • NavigationSuiteScaffoldLayout (L377-482): bar types (isNavigationBar) place the content above the navigation (content height = layout height − navigation height × progress) and the navigation at layout height − height × progress; the primary action at PrimaryActionContentPadding from the end (primaryActionContentAlignment) and above the bar. Other types place the navigation at −width × (1 − progress) and the content beside it.
  • NavigationSuite (L569-673): ShortNavigationBarCompact / Medium → ShortNavigationBar (items with the icon on top / at the start, NavigationSuiteItem L877-897); WideNavigationRailCollapsed / Expanded → WideNavigationRail (header = the primary action; Expanded starts expanded); NavigationDrawer → PermanentDrawerSheet (the primary action first).