Navigation containers
components specs/components/navigation.kmd
Primary navigation surfaces for switching between top-level destinations — Navigation bar (bottom, mobile), Navigation rail (side, tablet), and Navigation drawer (side, desktop / wide). Material parity (`/components/navigation-bar`, `/components/navigation-drawer`, `/components/navigation-rail`). These three are ONE adaptive component family, picked by window-size class.
When this spec applies
Primary triggers
- Implement navigation bar
- Implement navigation rail
- Implement navigation drawer
All triggers
- Add primary navigation between top-level destinations
- Build adaptive nav that switches per screen size
- Replace tab bar / hamburger with proper nav container
Specification body
Spec — Navigation containers
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/navigation-bar, https://m3.material.io/components/navigation-rail, https://m3.material.io/components/navigation-drawer.
Adaptive trio — pick by window-size class
| Class | Container | Anchor | Destinations visible |
|---|---|---|---|
| Compact (< 600 dp) | Navigation bar | Bottom edge | 3-5 |
| Medium (600-839 dp) | Navigation rail | Left edge | 3-7 |
| Expanded (840-1199 dp) | Rail (default) or drawer (expanded) | Left edge | 3-7 / unlimited |
| Large (≥ 1200 dp) | Drawer (standard) | Left edge | unlimited |
Same component family — destinations / icons / order stay consistent; only the layout container swaps. Implementations must share state (selected destination, badges) across the three.
R1 — Navigation bar (Compact)
Anatomy:
┌──────────────────────────────────────────────┐
│ ⌂ ⌥ ⚙ │
│ Home Inbox Settings │
└──────────────────────────────────────────────┘
- Height: 80 px (incl. safe-area bottom inset)
- Items: 3-5 destinations (never 1 or 2 — use back nav or tabs; never > 5 — use rail / drawer)
- Container bg:
surface-container - Per-item: 24 px icon + 12 px gap + label (
label-medium) - Selected: pill-shaped tonal
secondary-containerbehind icon + bold label color - Optional: badge on icon (per
components/badges.kmd) - Safe area: respects gesture-nav inset on Android / iOS
R2 — Navigation rail (Medium / Expanded compact)
Anatomy:
┌──────┐
│ ☰ │ ← optional menu button
│ ────│
│ ⌂ │
│ Home │
│ │
│ ⌥ │
│ Inbox│
│ │
│ ⚙ │
│ Set… │
│ ────│
│ ⊕ │ ← optional FAB at top OR bottom
└──────┘
- Width: 80 dp
- Items: 3-7 destinations
- Per-item: 24 px icon + 4 px gap + 1-line label
(
label-medium, can be hidden via prop) - Selected indicator: full-width tonal block behind item (height
56 px), with
secondary-containerbg - Optional FAB: anchored to top (after menu button) OR bottom
- Header: optional menu / app icon button at top
R3 — Navigation drawer (Expanded / Large)
Two sub-variants:
| Sub-variant | Modal | Use |
|---|---|---|
| Modal drawer | Yes (scrim) | Overlay drawer triggered by menu icon |
| Standard drawer | No | Always-visible side panel (≥ Large) |
Anatomy (standard drawer):
┌─────────────────────┐
│ App name │
│ ────────────────── │
│ ⌂ Home │
│ ⌥ Inbox • 3 │
│ ⚙ Settings │
│ ────────────────── │
│ Recent │
│ 📄 Document 1 │
│ 📄 Document 2 │
│ 📄 Document 3 │
│ ────────────────── │
│ + New │
└─────────────────────┘
- Width: 256-360 dp (default 320 dp)
- Items: full-width list rows; selected has
secondary-containertonal bg - Per-item: 24 px leading icon + 12 px gap + label
(
label-large) + optional trailing badge - Section headers: subheader divider (per
dividers.kmd § R5) - Container bg:
surface-container-low(standard) /surface-container(modal, with elevation)
R4 — Consistent state across variants
| Concept | Behavior |
|---|---|
| Selected destination | Same destination highlighted regardless of container |
| Badge | Same badge count / dot across variants |
| Order | Same destination order across variants |
| Icons | Same icon glyph; can adapt label visibility (rail can hide labels) |
Implementations expose a single "destinations" source of truth; the view layer renders the appropriate container per window-size class.
R5 — Selected state animation
| Container | Animation |
|---|---|
| Bar | Tonal pill animates between icons (motion-medium) |
| Rail | Tonal block fades in/out under new selected (motion-fast) |
| Drawer | Tonal bg fades to selected row (motion-fast) |
Reduced motion: instant change, no slide.
R6 — Header content
| Container | Header |
|---|---|
| Bar | None |
| Rail | Optional menu / app icon button at top |
| Drawer | App name + optional user / org switcher |
Drawer header is title-medium (16/24, weight 500); app icon 32 px
to the left.
R7 — Footer content
| Container | Footer |
|---|---|
| Bar | None |
| Rail | Optional FAB or settings icon at bottom |
| Drawer | Optional settings / sign-out section, separated by divider |
R8 — Modal drawer mode
Modal drawer slides in from the edge with a scrim (40% black overlay on the rest of the page).
- Trigger: menu icon button in app bar (
☰) - Dismiss: scrim tap, Esc key, swipe-left gesture, menu icon again
- Width: same 256-360 dp
- Z-order: above app bar, above bottom bar, below dialogs
Use modal drawer on Compact / Medium classes when standard drawer would dominate the screen. Switch to standard drawer at Expanded / Large by default.
R9 — Standard drawer mode
Always visible alongside main content. Used at Expanded / Large classes for primary navigation.
- No scrim
- No swipe-to-dismiss
- Always reflects current destination
- Can be collapsed to rail via prop (user-controlled or layout-driven)
R10 — Accessibility
- Container:
role="navigation"+aria-label="Primary" - Each destination:
role="link"(if it navigates URL) ORrole="button"(if it triggers in-app routing)aria-current="page"on selected destination
- Badges: included in destination's
aria-label("Inbox, 3 unread") - Modal drawer trigger:
aria-haspopup="dialog"+aria-expanded - Modal drawer container:
role="dialog"+aria-modal="true"+aria-labelledby(pointing to drawer header) when open - Keyboard:
- Tab: enters first destination
- Arrow Up / Down (rail / drawer) OR Left / Right (bar): moves between destinations
- Enter / Space: navigates
- Esc: closes modal drawer
- Skip-to-content link before nav for screen readers (matches app bar contract)
R11 — States
| State | Visual change |
|---|---|
| Rest | Base, icon on-surface-variant, label on-surface |
| Hover | State layer 8% under icon / row |
| Pressed | State layer 12% |
| Focused | 2 px focus ring on item OR ring outside tonal block |
| Selected | Tonal secondary-container bg + icon on-secondary-container + label weight 600 |
| Disabled | 38% opacity (rare on primary nav; usually use sub-screen disabling) |
R12 — Density
| Density | Bar height | Rail width | Drawer width |
|---|---|---|---|
| Compact | 64 px | 72 dp | 256 dp |
| Default | 80 px | 80 dp | 320 dp |
| Comfortable | 96 px | 96 dp | 360 dp |
R13 — Per-preset variation
| Preset | Bar | Rail | Drawer |
|---|---|---|---|
material3 | Pill tonal under icon | Tonal block, optional labels | Tonal selected row, FAB-style add |
material2 | Solid bg, no pill (just color change on selected) | No tonal block | Filled bg per item |
ios_cupertino | Tab bar style, label always visible | n/a (use bar) | Source list with indent levels |
gnome | n/a (desktop preset; use rail) | Adwaita header bar w/ pages | Sidebar style w/ accent on selected |
windows_11 | n/a (desktop preset) | NavigationView compact w/ icons + label below | NavigationView expanded |
brutalist | Sharp icons, thick border-top | Solid block selected, 2 px right border | Heavy 4 px borders between rows |
terminal_classic | Function key row [F1 Home] [F2 Inbox] | n/a | Numbered side menu 1) Home 2) Inbox |
R14 — Forbidden patterns
- ❌ Navigation bar on Expanded / Large window-size class (use rail or drawer)
- ❌ Navigation drawer on Compact (use bar or modal drawer)
- ❌ Mixing bar + drawer on the same screen (pick one container)
- ❌ Navigation bar with 1 or 2 items (use back navigation or tabs)
- ❌ Navigation bar with > 5 items (overflow to drawer / rail)
- ❌ Tab bar pattern (URL-distinct destinations as Tabs component) — use nav containers when destinations are top-level
- ❌ Navigation rail without selected indicator (defeats purpose)
- ❌ Standard drawer on Compact (consumes too much screen real estate)
- ❌ Navigation that hides on scroll (primary nav must always be reachable; secondary or sticky toolbars can hide)
- ❌ Bar / rail with destinations that route to different orgs / tenants (use tenant switcher in drawer header instead)
Cross-link
app-layout/window-size-classes.kmd— adaptive container per classapp-layout/safe-area.kmd— bar bottom inset rulethemes/elevation.kmd— modal drawer scrim rolethemes/color-roles.kmd—secondary-containerfor selectedthemes/typography.kmd—label-medium/label-largeinteraction/states.kmd— hover / focused / selected layerscomponents/badges.kmd— per-destination badge contractcomponents/tabs.kmd— sibling for within-context switching (NOT for top-level destinations)components/app-bars.kmd— modal drawer trigger lives in top barfoundations/elements.kmd— Container + Navigator families
Requirements (testable)
Requirement: Adaptive container per window-size class {#req-nav-adaptive-container}
The navigation family SHALL render the container appropriate to the current window-size class — Navigation bar on Compact, Navigation rail on Medium and Expanded-compact, and Navigation drawer on Large — and SHALL NOT render a container forbidden for that class (no bar on Expanded/Large, no standard drawer on Compact).
Scenario: Compact class renders the bar
- GIVEN a navigation family with a shared set of top-level destinations
- WHEN the window-size class is Compact (< 600 dp)
- THEN the rendered container is the Navigation bar anchored to the bottom edge
- AND no Navigation drawer or rail is rendered on that screen
Scenario: Large class renders the drawer, not the bar
- GIVEN the same navigation family
- WHEN the window-size class is Large (>= 1200 dp)
- THEN the rendered container is a standard Navigation drawer anchored to the left edge
- AND a Navigation bar is NOT rendered on the Large class
Requirement: Navigation bar destination-count bounds {#req-nav-bar-item-count}
A Navigation bar SHALL present between 3 and 5 destinations inclusive; it SHALL NOT be used with 1 or 2 destinations (use back navigation or tabs) nor with more than 5 destinations (overflow to rail or drawer).
Scenario: Bar with a valid count renders
- GIVEN a Navigation bar configured with 4 destinations
- WHEN the bar renders on a Compact window-size class
- THEN all 4 destinations are shown as items
- AND each item shows a 24 px icon with a
label-mediumlabel
Scenario: Bar with too many destinations is rejected
- GIVEN a navigation family configured with 6 top-level destinations on a Compact class
- WHEN the container is selected for rendering
- THEN the Navigation bar is not used for 6 items
- AND the implementation overflows to a rail or drawer container instead
Requirement: Consistent state across containers {#req-nav-consistent-state}
The navigation family SHALL expose a single destinations source of truth so that the selected destination, badge counts, and destination order are identical across the bar, rail, and drawer regardless of which container is currently rendered.
Scenario: Selection is preserved across a container swap
- GIVEN a navigation family whose selected destination is "Inbox" while rendering a bar on Compact
- WHEN the window-size class changes to Large and the drawer is rendered
- THEN the "Inbox" destination is the highlighted/selected row in the drawer
- AND the destination order matches the order shown in the bar
Scenario: Badge count matches across variants
- GIVEN a destination "Inbox" carrying a badge count of 3
- WHEN the same family is rendered as a bar and as a drawer
- THEN both containers show the badge count 3 on the "Inbox" destination
Requirement: Selected-state animation honors reduced motion {#req-nav-reduced-motion}
Selecting a destination SHALL animate the tonal selected indicator (pill on the bar, block on the rail, row background on the drawer); when the reduced-motion preference is active the change SHALL be instant with no slide or fade.
Scenario: Reduced motion produces an instant change
- GIVEN a Navigation bar with reduced-motion enabled and "Home" currently selected
- WHEN the user selects "Settings"
- THEN the tonal pill appears under "Settings" without a slide or fade transition
- AND no intermediate animation frames are emitted between the two positions
Requirement: Modal drawer open and dismiss behavior {#req-nav-modal-drawer}
A modal Navigation drawer SHALL open from the edge over a 40% scrim above the app bar and bottom bar but below dialogs, and SHALL be dismissible by scrim tap, Esc key, swipe-left gesture, or re-tapping the menu icon.
Scenario: Menu icon opens the modal drawer with a scrim
- GIVEN a Compact screen whose app bar shows a menu icon button
- WHEN the user taps the menu icon
- THEN the modal drawer slides in from the edge
- AND a 40% black scrim covers the rest of the page
- AND the drawer is layered above the app bar and bottom bar and below any dialog
Scenario: Esc dismisses the modal drawer
- GIVEN an open modal Navigation drawer
- WHEN the user presses the Esc key
- THEN the drawer closes and the scrim is removed
Requirement: Accessibility and keyboard contract {#req-nav-a11y}
The navigation container SHALL expose role="navigation" with aria-label="Primary", mark the selected destination with aria-current="page", include badge counts in a destination's accessible label, and support keyboard operation (Tab to enter, arrow keys to move between destinations, Enter/Space to navigate, Esc to close a modal drawer).
Scenario: Selected destination is announced as current
- GIVEN a navigation container with "Home" selected
- WHEN the container's accessibility tree is inspected
- THEN the container element exposes
role="navigation"andaria-label="Primary" - AND the "Home" destination carries
aria-current="page"
Scenario: Badge count is included in the accessible label
- GIVEN an "Inbox" destination with a badge count of 3
- WHEN the accessible label of the "Inbox" destination is read
- THEN the label includes the unread count (e.g. "Inbox, 3 unread")
Scenario: Arrow keys move focus between destinations
- GIVEN keyboard focus on the first destination of a Navigation rail
- WHEN the user presses Arrow Down
- THEN keyboard focus moves to the next destination in order
- AND pressing Enter on the focused destination triggers navigation to it
References
specs/foundations/elements.kmdspecs/themes/color-roles.kmdspecs/themes/elevation.kmdspecs/themes/typography.kmdspecs/interaction/states.kmdspecs/app-layout/window-size-classes.kmdspecs/app-layout/safe-area.kmd