Skip to content

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

All triggers

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

ClassContainerAnchorDestinations visible
Compact (< 600 dp)Navigation barBottom edge3-5
Medium (600-839 dp)Navigation railLeft edge3-7
Expanded (840-1199 dp)Rail (default) or drawer (expanded)Left edge3-7 / unlimited
Large (≥ 1200 dp)Drawer (standard)Left edgeunlimited

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-container behind 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-container bg
  • 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-variantModalUse
Modal drawerYes (scrim)Overlay drawer triggered by menu icon
Standard drawerNoAlways-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-container tonal 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

ConceptBehavior
Selected destinationSame destination highlighted regardless of container
BadgeSame badge count / dot across variants
OrderSame destination order across variants
IconsSame 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

ContainerAnimation
BarTonal pill animates between icons (motion-medium)
RailTonal block fades in/out under new selected (motion-fast)
DrawerTonal bg fades to selected row (motion-fast)

Reduced motion: instant change, no slide.

R6 — Header content

ContainerHeader
BarNone
RailOptional menu / app icon button at top
DrawerApp name + optional user / org switcher

Drawer header is title-medium (16/24, weight 500); app icon 32 px to the left.

ContainerFooter
BarNone
RailOptional FAB or settings icon at bottom
DrawerOptional 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) OR role="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

StateVisual change
RestBase, icon on-surface-variant, label on-surface
HoverState layer 8% under icon / row
PressedState layer 12%
Focused2 px focus ring on item OR ring outside tonal block
SelectedTonal secondary-container bg + icon on-secondary-container + label weight 600
Disabled38% opacity (rare on primary nav; usually use sub-screen disabling)

R12 — Density

DensityBar heightRail widthDrawer width
Compact64 px72 dp256 dp
Default80 px80 dp320 dp
Comfortable96 px96 dp360 dp

R13 — Per-preset variation

PresetBarRailDrawer
material3Pill tonal under iconTonal block, optional labelsTonal selected row, FAB-style add
material2Solid bg, no pill (just color change on selected)No tonal blockFilled bg per item
ios_cupertinoTab bar style, label always visiblen/a (use bar)Source list with indent levels
gnomen/a (desktop preset; use rail)Adwaita header bar w/ pagesSidebar style w/ accent on selected
windows_11n/a (desktop preset)NavigationView compact w/ icons + label belowNavigationView expanded
brutalistSharp icons, thick border-topSolid block selected, 2 px right borderHeavy 4 px borders between rows
terminal_classicFunction key row [F1 Home] [F2 Inbox]n/aNumbered 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)
  • app-layout/window-size-classes.kmd — adaptive container per class
  • app-layout/safe-area.kmd — bar bottom inset rule
  • themes/elevation.kmd — modal drawer scrim role
  • themes/color-roles.kmdsecondary-container for selected
  • themes/typography.kmdlabel-medium / label-large
  • interaction/states.kmd — hover / focused / selected layers
  • components/badges.kmd — per-destination badge contract
  • components/tabs.kmd — sibling for within-context switching (NOT for top-level destinations)
  • components/app-bars.kmd — modal drawer trigger lives in top bar
  • foundations/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-medium label

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" and aria-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