Navigation
Navigation suite
One set of destinations shown as a navigation bar, a rail or a drawer, chosen by the window size class.
- COMPONENT_SPEC §16
- CSS · custom elements · React
- Open in the playground
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.
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-drawerornone; default: adaptive),expanded-from(where the adaptive rule expands: a width class or px bound,nonefor Compose's own rule; defaultlarge),expanded-type(wide-navigation-rail-expandeddefault, ornavigation-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-typestill wins). Read-onlytype: 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).