17 KiB
MD3 Navigation Patterns
Guide for choosing and implementing Material Design 3 navigation components.
Jetpack Compose (primary)
Use androidx.compose.material3: NavigationBar, NavigationRail, NavigationDrawerItem, ModalNavigationDrawer, DismissibleNavigationDrawer, PermanentNavigationDrawer, NavigationBarItem, NavigationRailItem, top app bars (TopAppBar, CenterAlignedTopAppBar, LargeTopAppBar, expressive variants per BOM), and Scaffold (bottomBar, floatingActionButton, snackbarHost).
Wire destinations with Navigation Compose (NavHost, composable, rememberNavController). For adaptive UIs, use calculateWindowSizeClass, androidx.compose.material3.adaptive, or currentWindowAdaptiveInfo / NavigableListDetailPaneScaffold (names and packages depend on your BOM — check Android Developers).
Material's I/O 2026 update adds expressive/adaptive emphasis:
- Prefer expressive/adaptive scaffolds for mobile, desktop, foldables, watches, and XR rather than scaling one phone navigation model upward.
- Expressive search and search app bars have refreshed visual style, motion, and more flexible trailing icon behavior. Use current Compose Material3 APIs where available; use web/CSS approximations only when targeting web.
- Keep navigation spacing on the 8dp spacing system so rail/drawer/app-bar gaps can adapt by device class and density.
// Conceptual — adapt routes and selection to your app
Scaffold(
bottomBar = {
NavigationBar {
destinations.forEach { dest ->
NavigationBarItem(
selected = currentRoute == dest.route,
onClick = { navController.navigate(dest.route) },
icon = { Icon(dest.icon, contentDescription = dest.label) },
label = { Text(dest.label) }
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding)
) { /* composable routes */ }
}
Web (limited): The HTML/@material/web sections below remain useful for token-backed sites; Material Web is maintenance-only.
Navigation Component Selection
Decision Tree
How many primary destinations?
├── 2 destinations → Tabs (primary)
├── 3–5 destinations
│ ├── Compact screen (<600dp) → Navigation Bar (bottom)
│ ├── Medium screen (600–839dp) → Navigation Rail (side)
│ └── Expanded+ screen (840dp+) → Navigation Drawer (side) or Rail
├── 6+ destinations
│ ├── Compact → Navigation Drawer (modal)
│ ├── Medium → Navigation Drawer (standard) or Rail + overflow menu
│ └── Expanded+ → Navigation Drawer (standard)
└── Hierarchical (nested sections)
└── Navigation Drawer with sections
Quick Reference
| Component | Destinations | Screen Size | Persistence | Position |
|---|---|---|---|---|
| Navigation Bar | 3–5 | Compact | Persistent | Bottom |
| Navigation Rail | 3–7 | Medium | Persistent | Side (start) |
| Navigation Drawer | Unlimited | Expanded+ | Standard or Modal | Side (start) |
| Tabs | 2+ related views | Any | Persistent | Top (below app bar) |
| Bottom App Bar | — (contextual actions) | Compact | Persistent | Bottom |
Navigation Bar
Use when: 3–5 primary destinations on compact (mobile) screens. Position: Bottom of screen, always visible.
Anatomy
- Fixed at bottom, full width
- 3–5 navigation items with icon + label
- Active item shows filled icon + indicator pill
- Height: 80dp
Implementation
<md-navigation-bar active-index="0">
<md-navigation-tab label="Home" active-icon="home" inactive-icon="home">
<md-icon slot="active-icon">home</md-icon>
<md-icon slot="inactive-icon">home</md-icon>
</md-navigation-tab>
<md-navigation-tab label="Search">
<md-icon slot="active-icon">search</md-icon>
<md-icon slot="inactive-icon">search</md-icon>
</md-navigation-tab>
<md-navigation-tab label="Notifications">
<md-icon slot="active-icon">notifications</md-icon>
<md-icon slot="inactive-icon">notifications</md-icon>
</md-navigation-tab>
<md-navigation-tab label="Profile">
<md-icon slot="active-icon">person</md-icon>
<md-icon slot="inactive-icon">person</md-icon>
</md-navigation-tab>
</md-navigation-bar>
Styling
md-navigation-bar {
--md-navigation-bar-container-color: var(--md-sys-color-surface-container);
}
Guidelines
- Always show labels (don't use icon-only)
- Use filled icons for active state, outlined for inactive
- Don't use for fewer than 3 or more than 5 destinations
- Hide on scroll down in content-heavy screens (optional)
- Elevation level 2 (3dp)
Navigation Rail
Use when: 3–7 primary destinations on medium screens (tablets). Position: Start edge (left in LTR), always visible.
Anatomy
- Width: 80dp
- Optional FAB at top
- Navigation items vertically stacked
- Active item shows indicator pill
Implementation
<nav class="md3-nav-rail" aria-label="Main navigation">
<!-- Optional FAB -->
<md-fab size="small" variant="tertiary" aria-label="Compose">
<md-icon slot="icon">edit</md-icon>
</md-fab>
<div class="md3-nav-rail__items" role="tablist">
<a href="/" class="md3-nav-rail__item" role="tab" aria-selected="true" aria-current="page">
<div class="md3-nav-rail__indicator">
<md-icon>home</md-icon>
</div>
<span class="md3-nav-rail__label">Home</span>
</a>
<a href="/search" class="md3-nav-rail__item" role="tab" aria-selected="false">
<div class="md3-nav-rail__indicator">
<md-icon>search</md-icon>
</div>
<span class="md3-nav-rail__label">Search</span>
</a>
<a href="/settings" class="md3-nav-rail__item" role="tab" aria-selected="false">
<div class="md3-nav-rail__indicator">
<md-icon>settings</md-icon>
</div>
<span class="md3-nav-rail__label">Settings</span>
</a>
</div>
</nav>
.md3-nav-rail {
width: 80px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 12px;
background: var(--md-sys-color-surface);
border-right: 1px solid var(--md-sys-color-outline-variant);
}
.md3-nav-rail__items {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
}
.md3-nav-rail__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-decoration: none;
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-medium);
cursor: pointer;
width: 56px;
}
.md3-nav-rail__indicator {
width: 56px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--md-sys-shape-corner-full);
}
.md3-nav-rail__item[aria-selected="true"] .md3-nav-rail__indicator {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
.md3-nav-rail__item[aria-selected="true"] {
color: var(--md-sys-color-on-surface);
}
Guidelines
- Align items to top (below optional FAB)
- Show labels always (optional to hide, but recommended to show)
- FAB at top is optional but common
- Elevation level 0
Navigation Drawer
Use when: Many destinations, expanded screens, or deep hierarchies. Position: Start edge, standard (persistent) or modal (overlay).
Standard Drawer (Persistent)
Always visible alongside content. Width: 360dp.
<div class="md3-layout">
<md-navigation-drawer opened>
<div slot="headline">App Name</div>
<md-list>
<md-list-item type="button" active>
<md-icon slot="start">inbox</md-icon>
<div slot="headline">Inbox</div>
<div slot="trailing-supporting-text">24</div>
</md-list-item>
<md-list-item type="button">
<md-icon slot="start">send</md-icon>
<div slot="headline">Sent</div>
</md-list-item>
<md-divider></md-divider>
<md-list-item type="button">
<md-icon slot="start">drafts</md-icon>
<div slot="headline">Drafts</div>
</md-list-item>
</md-list>
</md-navigation-drawer>
<main class="md3-content">
<!-- Page content -->
</main>
</div>
Modal Drawer (Overlay)
Overlays content with a scrim. Used on smaller screens or when content space is limited.
<md-navigation-drawer type="modal" id="nav-drawer">
<!-- Same content as standard -->
</md-navigation-drawer>
<script>
// Toggle drawer
document.getElementById('menu-btn').addEventListener('click', () => {
const drawer = document.getElementById('nav-drawer');
drawer.opened = !drawer.opened;
});
</script>
Guidelines
- Standard drawer uses
surface-containerbackground - Modal drawer has elevation level 1 and scrim overlay
- Group destinations with dividers and section headers
- Active item uses
secondary-containerbackground - Shape:
largeon end corners (right edge in LTR)
Top App Bar
Use when: Every screen needs a title and optional actions.
I/O 2026 note: Search app bars are part of the current expressive search guidance. In Compose, check your Material3 BOM for expressive app bar and search APIs before hand-rolling. For web, combine a token-backed top app bar with a custom search field/view because Material Web does not expose full expressive search parity.
Variants
| Variant | Title | Height | Scroll Behavior |
|---|---|---|---|
| Center-aligned | Center | 64dp | Elevates to level 2 on scroll |
| Small | Start-aligned | 64dp | Elevates to level 2 on scroll |
| Medium | Bottom, start-aligned | 112dp | Collapses to 64dp on scroll |
| Large | Bottom, start-aligned | 152dp | Collapses to 64dp on scroll |
Implementation
<!-- Small app bar -->
<header class="md3-top-app-bar">
<md-icon-button aria-label="Open menu">
<md-icon>menu</md-icon>
</md-icon-button>
<h1 class="md3-top-app-bar__title">Page Title</h1>
<md-icon-button aria-label="Search">
<md-icon>search</md-icon>
</md-icon-button>
<md-icon-button aria-label="More options">
<md-icon>more_vert</md-icon>
</md-icon-button>
</header>
<!-- Medium app bar (collapsed state shown; expand on scroll-to-top) -->
<header class="md3-top-app-bar md3-top-app-bar--medium">
<div class="md3-top-app-bar__row">
<md-icon-button aria-label="Back"><md-icon>arrow_back</md-icon></md-icon-button>
<span class="md3-top-app-bar__title-collapsed"></span>
<md-icon-button aria-label="More"><md-icon>more_vert</md-icon></md-icon-button>
</div>
<div class="md3-top-app-bar__expanded-title">
<h1>Page Title</h1>
</div>
</header>
.md3-top-app-bar {
display: flex;
align-items: center;
height: 64px;
padding: 0 4px;
background: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
}
.md3-top-app-bar__title {
flex: 1;
padding: 0 12px;
font: var(--md-sys-typescale-title-large);
margin: 0;
}
/* Scrolled state: elevate */
.md3-top-app-bar--scrolled {
background: var(--md-sys-color-surface-container);
}
Scroll Behavior
// Elevate app bar on scroll
const appBar = document.querySelector('.md3-top-app-bar');
window.addEventListener('scroll', () => {
appBar.classList.toggle('md3-top-app-bar--scrolled', window.scrollY > 0);
});
Tabs
Use when: Switching between related content at the same hierarchy level.
Primary vs Secondary
- Primary tabs: Top-level content switching (Flights / Hotels / Explore)
- Secondary tabs: Sub-sections within primary content
<!-- Primary tabs -->
<md-tabs>
<md-primary-tab active>
<md-icon slot="icon">flight</md-icon>
Flights
</md-primary-tab>
<md-primary-tab>Hotels</md-primary-tab>
<md-primary-tab>Car Rental</md-primary-tab>
</md-tabs>
<!-- Secondary tabs (nested under primary) -->
<md-tabs>
<md-secondary-tab active>Overview</md-secondary-tab>
<md-secondary-tab>Reviews</md-secondary-tab>
<md-secondary-tab>Photos</md-secondary-tab>
</md-tabs>
Tab + Panel Connection
<md-tabs id="my-tabs">
<md-primary-tab id="tab-1" aria-controls="panel-1" active>Tab 1</md-primary-tab>
<md-primary-tab id="tab-2" aria-controls="panel-2">Tab 2</md-primary-tab>
</md-tabs>
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
Panel 1 content
</div>
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>
Panel 2 content
</div>
<script>
document.getElementById('my-tabs').addEventListener('change', (e) => {
// Hide all panels
document.querySelectorAll('[role="tabpanel"]').forEach(p => p.hidden = true);
// Show selected panel
const activeTab = e.target.querySelector('[active]');
const panelId = activeTab.getAttribute('aria-controls');
document.getElementById(panelId).hidden = false;
});
</script>
Responsive Navigation Pattern
The key MD3 pattern: navigation component transforms across breakpoints.
Mobile → Tablet → Desktop
Compact (<600dp): Navigation Bar (bottom)
Medium (600–839dp): Navigation Rail (side)
Expanded (840dp+): Navigation Drawer (side, standard)
CSS Implementation
/* Hide all nav variants by default */
.md3-nav-bar,
.md3-nav-rail,
.md3-nav-drawer { display: none; }
/* Compact: show bottom navigation bar */
@media (max-width: 599px) {
.md3-nav-bar { display: flex; }
.md3-app { flex-direction: column; }
}
/* Medium: show navigation rail */
@media (min-width: 600px) and (max-width: 839px) {
.md3-nav-rail { display: flex; }
.md3-app { flex-direction: row; }
}
/* Expanded+: show navigation drawer */
@media (min-width: 840px) {
.md3-nav-drawer { display: flex; }
.md3-app { flex-direction: row; }
}
Complete Responsive Shell
<div class="md3-app">
<!-- Navigation drawer (expanded+) -->
<aside class="md3-nav-drawer">
<md-navigation-drawer opened>
<div slot="headline">My App</div>
<md-list>
<md-list-item type="button" active>
<md-icon slot="start">home</md-icon>Home
</md-list-item>
<md-list-item type="button">
<md-icon slot="start">search</md-icon>Search
</md-list-item>
<md-list-item type="button">
<md-icon slot="start">settings</md-icon>Settings
</md-list-item>
</md-list>
</md-navigation-drawer>
</aside>
<!-- Navigation rail (medium) -->
<nav class="md3-nav-rail" aria-label="Main">
<md-fab size="small" aria-label="New"><md-icon slot="icon">add</md-icon></md-fab>
<a class="md3-nav-rail__item active"><md-icon>home</md-icon><span>Home</span></a>
<a class="md3-nav-rail__item"><md-icon>search</md-icon><span>Search</span></a>
<a class="md3-nav-rail__item"><md-icon>settings</md-icon><span>Settings</span></a>
</nav>
<!-- Main content -->
<main class="md3-main">
<header class="md3-top-app-bar">
<md-icon-button class="md3-menu-btn" aria-label="Menu"><md-icon>menu</md-icon></md-icon-button>
<h1 class="md3-top-app-bar__title">Home</h1>
</header>
<div class="md3-body">
<!-- Page content -->
</div>
</main>
<!-- Navigation bar (compact) -->
<md-navigation-bar class="md3-nav-bar">
<md-navigation-tab label="Home" active>
<md-icon slot="active-icon">home</md-icon>
<md-icon slot="inactive-icon">home</md-icon>
</md-navigation-tab>
<md-navigation-tab label="Search">
<md-icon slot="active-icon">search</md-icon>
<md-icon slot="inactive-icon">search</md-icon>
</md-navigation-tab>
<md-navigation-tab label="Settings">
<md-icon slot="active-icon">settings</md-icon>
<md-icon slot="inactive-icon">settings</md-icon>
</md-navigation-tab>
</md-navigation-bar>
</div>
.md3-app {
display: flex;
min-height: 100vh;
background: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
}
.md3-main { flex: 1; display: flex; flex-direction: column; }
.md3-body { flex: 1; padding: 16px; }
/* Compact */
@media (max-width: 599px) {
.md3-app { flex-direction: column; }
.md3-nav-rail, .md3-nav-drawer { display: none; }
.md3-nav-bar { display: flex; order: 1; }
.md3-menu-btn { display: none; }
}
/* Medium */
@media (min-width: 600px) and (max-width: 839px) {
.md3-nav-bar, .md3-nav-drawer { display: none; }
.md3-nav-rail { display: flex; }
.md3-menu-btn { display: none; }
}
/* Expanded+ */
@media (min-width: 840px) {
.md3-nav-bar, .md3-nav-rail { display: none; }
.md3-nav-drawer { display: flex; }
.md3-menu-btn { display: none; }
}