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
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