Navigation Drawer
Material Design 3 Navigation Drawer component (<mdc-navigation-drawer>) built with Lit and Web Components. Strictly conforms to Android 12+, Flutter, and Jetpack Compose MD3 navigation drawer specifications.
Features
- 3 Variants:
modal(default): Floating overlay above content with a scrim backdrop, smooth CSS@starting-styleentry/exit animations, and a top drag handle (dismiss outward / relocate across the viewport midline).standard: In-flow collapsible drawer sharing screen space with main content.permanent: Persistent fixed side panel always visible in layout.
- Docking Edges: Supports
drawer-edge="start"(default) anddrawer-edge="end"with full RTL awareness (dir="rtl") — drag the handle across the viewport midline to re-dock the open modal drawer, or callrelocate(edge)programmatically. - Navigation Scope Synchronization: Integrates with
GlobalNavigationStateStoreand<mdc-navigation-tab>to sync active destinations across bars, rails, and drawers sharing the samenavigation-scope. - Inner Anatomy & Slots:
headerslot: Profile, avatar, account switcher, or logo.headlineproperty & slot: Drawer title/headline (e.g. "Mail", "Inbox") styled with MD3 TitleSmall typography.- Default slot: Navigation destinations (
<mdc-navigation-tab>withdrawervariant). footerslot: Bottom pinned actions, settings, or user info.- Automatic top/bottom scroll dividers with intersection observer detection.
- Modal drag handle: the top pill that drives dismiss / relocate gestures.
Installation & Import
import '@sandlada/mdc/components/navigation-drawer/index'
import '@sandlada/mdc/components/navigation-tab/index'
import '@sandlada/mdc/components/icon/index'
Usage
Modal Navigation Drawer
<mdc-button onclick="document.querySelector('#drawer').show()">Open Drawer</mdc-button>
<mdc-navigation-drawer id="drawer" variant="modal" headline="Mail">
<mdc-navigation-tab name="nav" value="/inbox" checked label="Inbox">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="nav" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="nav" value="/trash" label="Trash">
<mdc-icon slot="inactive-icon">delete</mdc-icon>
<mdc-icon slot="active-icon" filled>delete</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
Standard Navigation Drawer
<div style="display: flex; height: 100vh;">
<mdc-navigation-drawer variant="standard" open headline="App Navigation">
<mdc-navigation-tab name="std-nav" value="/home" checked label="Home">
<mdc-icon slot="inactive-icon">home</mdc-icon>
<mdc-icon slot="active-icon" filled>home</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="std-nav" value="/explore" label="Explore">
<mdc-icon slot="inactive-icon">explore</mdc-icon>
<mdc-icon slot="active-icon" filled>explore</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<main style="flex: 1; padding: 24px;">
<h1>Main Content</h1>
</main>
</div>
API Reference
Properties & Attributes
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
variant |
variant |
'modal' | 'standard' | 'permanent' |
'modal' |
Visual display variant. |
open |
open |
boolean |
false |
Controls whether the drawer is open. (Always true in permanent mode). |
drawerEdge |
drawer-edge |
'start' | 'end' |
'start' |
Viewport edge to dock to. |
headline |
headline |
string |
'' |
Title string rendered at the top of the destinations list. |
quick |
quick |
boolean |
false |
When true, skips all entry/exit animations. |
cancelable |
cancelable |
boolean |
true |
When true (modal only), allows Esc key and scrim tap dismissal. |
draggable |
draggable |
boolean |
true |
When true (modal only), enables top drag-handle gestures: drag outward to dismiss, pull across the viewport midline to relocate to the opposite edge. |
noFocusTrap |
no-focus-trap |
boolean |
false |
When true (modal only), disables automatic focus trap. |
returnValue |
return-value |
string |
'' |
Return value dispatched in close events. |
navigationScope |
navigation-scope |
string |
'global' |
Scope ID for synchronizing active state with other navigation controls. |
Methods
| Method | Returns | Description |
|---|---|---|
show() |
Promise<void> |
Imperatively opens the drawer and resolves when entrance animation completes. |
hide(reason?, returnValue?) |
Promise<void> |
Closes the drawer and resolves when exit animation completes. |
close(returnValue?) |
Promise<void> |
Convenience method to close the drawer. |
toggle() |
Promise<void> |
Toggles between open and closed states. |
relocate(edge) |
Promise<void> |
Re-docks the drawer to the given logical edge ('start' | 'end') with a transition. Closed drawers swap instantly. |
Events
| Event | Detail Payload | Description |
|---|---|---|
navigation-drawer-opening |
— | Fired when the drawer begins opening. |
navigation-drawer-opened |
— | Fired when the drawer has finished opening. |
navigation-drawer-closing |
— | Fired when the drawer begins closing. |
navigation-drawer-closed |
{ reason: string, returnValue: string } |
Fired when the drawer has finished closing. |
navigation-drawer-cancel |
{ reason: 'escape' | 'scrim' } |
Fired on Esc or scrim click before closing. Cancelable via event.preventDefault(). |
navigation-drawer-drag-start |
{ drawerEdge: string } |
Fired when a handle drag engages. |
navigation-drawer-drag |
{ dx: number, progress: number } |
Fired continuously during drag movement. |
navigation-drawer-drag-end |
{ committed: boolean, target: 'closed' | 'open' | 'relocate', reason?: string, dx: number, relocateTo?: string } |
Fired when the drag gesture is released. |
navigation-drawer-relocate |
{ edge: string } |
Fired after a handle drag re-docked the drawer to the opposite edge. |
Draggable
Code
<!-- Drag-handle gestures enabled: drag the top handle outward past the commit
threshold to dismiss, or pull it across the viewport midline to relocate
the drawer to the opposite edge. -->
<mdc-button variant="filled" onclick="this.getRootNode().querySelector('#draggable-on-drawer').show()"><span>Draggable</span></mdc-button>
<mdc-navigation-drawer id="draggable-on-drawer" variant="modal" headline="Draggable (default)" draggable navigation-scope="draggable-demo-1">
<mdc-navigation-tab name="draggable-tabs-1" value="/inbox" checked label="Inbox">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="draggable-tabs-1" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Drag-handle gestures disabled: the handle renders but does not drag. -->
<mdc-button variant="filled" onclick="this.getRootNode().querySelector('#draggable-off-drawer').show()"><span>Not draggable</span></mdc-button>
<mdc-navigation-drawer id="draggable-off-drawer" variant="modal" headline="Not draggable" draggable="false" navigation-scope="draggable-demo-2">
<mdc-navigation-tab name="draggable-tabs-2" value="/inbox" checked label="Inbox">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="draggable-tabs-2" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Drawer Edge
Code
<!-- Drawer docked on Start edge (default) -->
<mdc-navigation-drawer variant="standard" open drawer-edge="start" headline="Start Edge" navigation-scope="edge-demo-1">
<mdc-navigation-tab name="edge-tabs-1" value="/start-1" checked label="Start Item 1">
<mdc-icon slot="inactive-icon">menu</mdc-icon>
<mdc-icon slot="active-icon" filled>menu</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="edge-tabs-1" value="/start-2" label="Start Item 2">
<mdc-icon slot="inactive-icon">menu_open</mdc-icon>
<mdc-icon slot="active-icon" filled>menu_open</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Drawer docked on End edge -->
<mdc-navigation-drawer variant="standard" open drawer-edge="end" headline="End Edge" navigation-scope="edge-demo-2">
<mdc-navigation-tab name="edge-tabs-2" value="/end-1" checked label="End Item 1">
<mdc-icon slot="inactive-icon">folder</mdc-icon>
<mdc-icon slot="active-icon" filled>folder</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="edge-tabs-2" value="/end-2" label="End Item 2">
<mdc-icon slot="inactive-icon">folder_open</mdc-icon>
<mdc-icon slot="active-icon" filled>folder_open</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Headline
Code
<!-- Navigation Drawer with Headline -->
<mdc-navigation-drawer variant="standard" open headline="Mailbox" navigation-scope="headline-demo-1">
<mdc-navigation-tab name="headline-tabs-1" value="/all-mail" checked label="All Mail">
<mdc-icon slot="inactive-icon">mail</mdc-icon>
<mdc-icon slot="active-icon" filled>mail</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="headline-tabs-1" value="/starred" label="Starred">
<mdc-icon slot="inactive-icon">star</mdc-icon>
<mdc-icon slot="active-icon" filled>star</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Navigation Drawer with custom slotted headline -->
<mdc-navigation-drawer variant="standard" open navigation-scope="headline-demo-2">
<mdc-typography slot="headline" variant="title-small" style="font-weight: 700; color: var(--md-sys-color-primary, #006a6a);">Custom Projects</mdc-typography>
<mdc-navigation-tab name="headline-tabs-2" value="/project-a" checked label="Project Alpha">
<mdc-icon slot="inactive-icon">folder</mdc-icon>
<mdc-icon slot="active-icon" filled>folder</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="headline-tabs-2" value="/project-b" label="Project Beta">
<mdc-icon slot="inactive-icon">folder</mdc-icon>
<mdc-icon slot="active-icon" filled>folder</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Navigation Scope
Code
<!-- Synchronized Navigation Drawer in shared scope -->
<mdc-navigation-drawer variant="standard" open headline="Drawer A" navigation-scope="sync-scope">
<mdc-navigation-tab name="sync-tabs" value="/dashboard" checked label="Dashboard">
<mdc-icon slot="inactive-icon">dashboard</mdc-icon>
<mdc-icon slot="active-icon" filled>dashboard</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="sync-tabs" value="/messages" label="Messages">
<mdc-icon slot="inactive-icon">mail</mdc-icon>
<mdc-icon slot="active-icon" filled>mail</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="sync-tabs" value="/profile" label="Profile">
<mdc-icon slot="inactive-icon">person</mdc-icon>
<mdc-icon slot="active-icon" filled>person</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<mdc-navigation-drawer variant="standard" open headline="Drawer B (Synced)" navigation-scope="sync-scope">
<mdc-navigation-tab name="sync-tabs" value="/dashboard" checked label="Dashboard">
<mdc-icon slot="inactive-icon">dashboard</mdc-icon>
<mdc-icon slot="active-icon" filled>dashboard</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="sync-tabs" value="/messages" label="Messages">
<mdc-icon slot="inactive-icon">mail</mdc-icon>
<mdc-icon slot="active-icon" filled>mail</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="sync-tabs" value="/profile" label="Profile">
<mdc-icon slot="inactive-icon">person</mdc-icon>
<mdc-icon slot="active-icon" filled>person</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Open
Code
<!-- Open standard navigation drawer -->
<mdc-navigation-drawer variant="standard" open headline="Open Drawer" navigation-scope="open-demo-1">
<mdc-navigation-tab name="open-tabs-1" value="/home" checked label="Home">
<mdc-icon slot="inactive-icon">home</mdc-icon>
<mdc-icon slot="active-icon" filled>home</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="open-tabs-1" value="/explore" label="Explore">
<mdc-icon slot="inactive-icon">explore</mdc-icon>
<mdc-icon slot="active-icon" filled>explore</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Closed standard navigation drawer -->
<mdc-navigation-drawer variant="standard" headline="Closed Drawer" navigation-scope="open-demo-2">
<mdc-navigation-tab name="open-tabs-2" value="/home" checked label="Home">
<mdc-icon slot="inactive-icon">home</mdc-icon>
<mdc-icon slot="active-icon" filled>home</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="open-tabs-2" value="/explore" label="Explore">
<mdc-icon slot="inactive-icon">explore</mdc-icon>
<mdc-icon slot="active-icon" filled>explore</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Quick
Code
<!-- Navigation Drawer with Quick animation suppression -->
<mdc-navigation-drawer variant="standard" open quick headline="Quick Drawer" navigation-scope="quick-demo-1">
<mdc-navigation-tab name="quick-tabs-1" value="/q1" checked label="Instant Tab 1">
<mdc-icon slot="inactive-icon">bolt</mdc-icon>
<mdc-icon slot="active-icon" filled>bolt</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="quick-tabs-1" value="/q2" label="Instant Tab 2">
<mdc-icon slot="inactive-icon">flash_on</mdc-icon>
<mdc-icon slot="active-icon" filled>flash_on</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Navigation Drawer standard without Quick -->
<mdc-navigation-drawer variant="standard" open headline="Animated Drawer" navigation-scope="quick-demo-2">
<mdc-navigation-tab name="quick-tabs-2" value="/a1" checked label="Animated Tab 1">
<mdc-icon slot="inactive-icon">animation</mdc-icon>
<mdc-icon slot="active-icon" filled>animation</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="quick-tabs-2" value="/a2" label="Animated Tab 2">
<mdc-icon slot="inactive-icon">motion_photos_on</mdc-icon>
<mdc-icon slot="active-icon" filled>motion_photos_on</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>Standard
Code
<!-- Standard Navigation Drawer Layout with Toggle Button -->
<div style="display: flex; height: 400px; border: 1px solid var(--md-sys-color-outline-variant, #c4c7c5); border-radius: 16px; overflow: hidden; position: relative;">
<mdc-navigation-drawer id="standard-drawer-demo" variant="standard" open headline="App Navigation" navigation-scope="standard-page-scope">
<mdc-navigation-tab name="std-page-tabs" value="/home" checked label="Home">
<mdc-icon slot="inactive-icon">home</mdc-icon>
<mdc-icon slot="active-icon" filled>home</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="std-page-tabs" value="/explore" label="Explore">
<mdc-icon slot="inactive-icon">explore</mdc-icon>
<mdc-icon slot="active-icon" filled>explore</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="std-page-tabs" value="/library" label="Library">
<mdc-icon slot="inactive-icon">video_library</mdc-icon>
<mdc-icon slot="active-icon" filled>video_library</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="std-page-tabs" value="/settings" label="Settings">
<mdc-icon slot="inactive-icon">settings</mdc-icon>
<mdc-icon slot="active-icon" filled>settings</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<div style="flex: 1; padding: 24px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--md-sys-color-surface, #fff);">
<div style="display: flex; align-items: center; gap: 12px;">
<mdc-icon-button onclick="this.getRootNode().querySelector('#standard-drawer-demo').toggle()">
<mdc-icon>menu</mdc-icon>
</mdc-icon-button>
<h2 style="margin: 0; font-size: 20px; font-weight: 500;">Standard In-Flow Drawer Layout</h2>
</div>
<p style="margin: 0; color: var(--md-sys-color-on-surface-variant, #444746); font-size: 14px; line-height: 20px;">
Standard navigation drawers are co-planar with the main content and share screen real estate without a scrim. Click the hamburger icon above to toggle the drawer open or closed with smooth width transitions.
</p>
</div>
</div>Variant
Code
<!-- Modal Navigation Drawer -->
<mdc-button variant="filled" onclick="this.getRootNode().querySelector('#variant-modal-drawer').show()"><span>Open Modal Drawer</span></mdc-button>
<mdc-navigation-drawer id="variant-modal-drawer" variant="modal" headline="Mail" navigation-scope="modal-variant-demo">
<mdc-navigation-tab name="modal-var-tabs" value="/inbox" checked label="Inbox" badge="24">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="modal-var-tabs" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="modal-var-tabs" value="/favorites" label="Favorites">
<mdc-icon slot="inactive-icon">favorite</mdc-icon>
<mdc-icon slot="active-icon" filled>favorite</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="modal-var-tabs" value="/trash" label="Trash">
<mdc-icon slot="inactive-icon">delete</mdc-icon>
<mdc-icon slot="active-icon" filled>delete</mdc-icon>
</mdc-navigation-tab>
<mdc-divider></mdc-divider>
<mdc-typography variant="title-small" class="section-title">Labels</mdc-typography>
<mdc-navigation-tab name="modal-var-tabs" value="/label-1" label="Label">
<mdc-icon slot="inactive-icon">circle</mdc-icon>
<mdc-icon slot="active-icon" filled>circle</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="modal-var-tabs" value="/label-2" label="Label">
<mdc-icon slot="inactive-icon">change_history</mdc-icon>
<mdc-icon slot="active-icon" filled>change_history</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="modal-var-tabs" value="/label-3" label="Label">
<mdc-icon slot="inactive-icon">square</mdc-icon>
<mdc-icon slot="active-icon" filled>square</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Standard Navigation Drawer (in-flow) -->
<mdc-navigation-drawer variant="standard" open headline="Mail" navigation-scope="standard-demo">
<mdc-navigation-tab name="standard-tabs" value="/inbox" checked label="Inbox" badge="24">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="standard-tabs" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="standard-tabs" value="/favorites" label="Favorites">
<mdc-icon slot="inactive-icon">favorite</mdc-icon>
<mdc-icon slot="active-icon" filled>favorite</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="standard-tabs" value="/trash" label="Trash">
<mdc-icon slot="inactive-icon">delete</mdc-icon>
<mdc-icon slot="active-icon" filled>delete</mdc-icon>
</mdc-navigation-tab>
<mdc-divider></mdc-divider>
<mdc-typography variant="title-small" class="section-title">Labels</mdc-typography>
<mdc-navigation-tab name="standard-tabs" value="/label-1" label="Label">
<mdc-icon slot="inactive-icon">circle</mdc-icon>
<mdc-icon slot="active-icon" filled>circle</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="standard-tabs" value="/label-2" label="Label">
<mdc-icon slot="inactive-icon">change_history</mdc-icon>
<mdc-icon slot="active-icon" filled>change_history</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="standard-tabs" value="/label-3" label="Label">
<mdc-icon slot="inactive-icon">square</mdc-icon>
<mdc-icon slot="active-icon" filled>square</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>
<!-- Permanent Navigation Drawer (persistent side panel) -->
<mdc-navigation-drawer variant="permanent" headline="Mail" navigation-scope="permanent-demo">
<mdc-navigation-tab name="permanent-tabs" value="/inbox" checked label="Inbox" badge="24">
<mdc-icon slot="inactive-icon">inbox</mdc-icon>
<mdc-icon slot="active-icon" filled>inbox</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="permanent-tabs" value="/outbox" label="Outbox">
<mdc-icon slot="inactive-icon">send</mdc-icon>
<mdc-icon slot="active-icon" filled>send</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="permanent-tabs" value="/favorites" label="Favorites">
<mdc-icon slot="inactive-icon">favorite</mdc-icon>
<mdc-icon slot="active-icon" filled>favorite</mdc-icon>
</mdc-navigation-tab>
<mdc-navigation-tab name="permanent-tabs" value="/trash" label="Trash">
<mdc-icon slot="inactive-icon">delete</mdc-icon>
<mdc-icon slot="active-icon" filled>delete</mdc-icon>
</mdc-navigation-tab>
</mdc-navigation-drawer>