MDC

Navigation Drawer

READMEDemo

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-style entry/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) and drawer-edge="end" with full RTL awareness (dir="rtl") — drag the handle across the viewport midline to re-dock the open modal drawer, or call relocate(edge) programmatically.
  • Navigation Scope Synchronization: Integrates with GlobalNavigationStateStore and <mdc-navigation-tab> to sync active destinations across bars, rails, and drawers sharing the same navigation-scope.
  • Inner Anatomy & Slots:
    • header slot: Profile, avatar, account switcher, or logo.
    • headline property & slot: Drawer title/headline (e.g. "Mail", "Inbox") styled with MD3 TitleSmall typography.
    • Default slot: Navigation destinations (<mdc-navigation-tab> with drawer variant).
    • footer slot: 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>