Drawer

Material Design 3 navigation drawer component for side navigation

Drawer

Navigation drawers provide access to destinations in your app. @duskmoon-dev/core provides a flexible drawer component with multiple variants and positions.

Native non-modal drawer

Use popover="auto" for a non-modal surface in the browser’s top layer. The browser handles Escape, outside-click dismissal, and focus restoration when dismissed with Escape. It does not make the rest of the page inert or trap focus.

Native non-modal Drawer

Native modal drawer

Use a native dialog with show-modal for modal interaction: the browser makes the background inert, keeps keyboard focus inside, and returns focus to the opener on close. closedby="any" enables outside-click dismissal in supporting browsers; Escape and the explicit close button remain available. Omit closedby="any" to require Escape or a close action. The scrim comes from ::backdrop; no separate backdrop element is needed.

Native modal Drawer

Review changes

Review your changes before continuing.

State, positioning, and motion

Native surfaces use :popover-open or the dialog’s open attribute. Do not add legacy visibility classes or toggle open directly for modal dialogs. Invoker commands require browser support; applications targeting older browsers can call showPopover() / hidePopover() and showModal() / close() as a compatibility fallback.

These panels attach to viewport edges rather than their trigger, so they do not need an anchor name. Opening and closing transitions preserve the top layer with discrete display / overlay transitions. Reduced-motion preferences disable these transitions.

Legacy class-controlled examples

The following examples retain the existing class API. Applications using it must manage focus, Escape, and dismissal themselves. Prefer the native examples above for overlays.

Basic Usage

Basic Drawer

Position

Left Drawer (Default)

Left Drawer

Right Drawer

Right Drawer

Surface Variants

Surface Container Low (Default)

Surface Container Low

Surface

Surface Variant

Surface Container

Surface Container

Surface Container High

Surface Container High

Sizes

Small

Small Drawer

Medium (Default)

Medium Drawer

Large

Large Drawer

Extra Large

Extra Large Drawer

Rail Variant

Collapsed icon-only drawer:

Rail Drawer

Permanent Drawer

Always visible, no overlay:

Permanent Drawer

Navigation

With Sections and Labels

Drawer with Sections

With Icons and Badges

Drawer with Icons and Badges

Active Item Colors

Primary (Default)

Active Primary

Secondary

Active Secondary

Tertiary

Active Tertiary

Complete Drawer

Nested Menu Items

Nested Menu Items

Best Practices

Organize drawer items logically:

  1. Primary navigation at the top
  2. Group related items with labels and dividers
  3. Settings/profile at the bottom

Accessibility

  • Use semantic elements (<nav>, <a>, <button>)
  • Provide aria-label for close buttons
  • Support keyboard navigation
  • Include focus indicators

Accessible Drawer

Responsive Design

Use permanent drawer for desktop, modal drawer for mobile:

Responsive Drawer

Framework Examples

React

import { useState } from 'react';

interface DrawerProps {
  open: boolean;
  onClose: () => void;
  position?: 'left' | 'right';
  children: React.ReactNode;
}

export function Drawer({ open, onClose, position = 'left', children }: DrawerProps) {
  return (
    <>
      <div className={`drawer drawer-${position} ${open ? 'drawer-open' : ''}`}>
        <div className="drawer-header">
          <h2 className="drawer-title">Menu</h2>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            ×
          </button>
        </div>
        <div className="drawer-body">{children}</div>
      </div>
      <div
        className={`drawer-backdrop ${open ? 'drawer-backdrop-show' : ''}`}
        onClick={onClose}
      />
    </>
  );
}

// Usage
function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Drawer</button>
      <Drawer open={open} onClose={() => setOpen(false)}>
        <a href="#" className="drawer-item">Home</a>
        <a href="#" className="drawer-item">Settings</a>
      </Drawer>
    </>
  );
}

Vue

<template>
  <div>
    <div :class="['drawer', `drawer-${position}`, { 'drawer-open': open }]">
      <div class="drawer-header">
        <h2 class="drawer-title">Menu</h2>
        <button class="drawer-close" @click="$emit('close')" aria-label="Close">
          ×
        </button>
      </div>
      <div class="drawer-body">
        <slot />
      </div>
    </div>
    <div
      :class="['drawer-backdrop', { 'drawer-backdrop-show': open }]"
      @click="$emit('close')"
    />
  </div>
</template>

<script setup>
defineProps({
  open: {
    type: Boolean,
    required: true
  },
  position: {
    type: String,
    default: 'left'
  }
});

defineEmits(['close']);
</script>

<!-- Usage -->
<Drawer :open="isOpen" @close="isOpen = false">
  <a href="#" class="drawer-item">Home</a>
  <a href="#" class="drawer-item">Settings</a>
</Drawer>

JavaScript (Vanilla)

// Toggle drawer
function toggleDrawer(drawerId) {
  const drawer = document.getElementById(drawerId);
  const backdrop = document.querySelector('.drawer-backdrop');

  drawer.classList.toggle('drawer-open');
  backdrop.classList.toggle('drawer-backdrop-show');
}

// Close drawer when clicking backdrop
document.querySelector('.drawer-backdrop').addEventListener('click', () => {
  document.querySelector('.drawer').classList.remove('drawer-open');
  document.querySelector('.drawer-backdrop').classList.remove('drawer-backdrop-show');
});

// Close drawer button
document.querySelector('.drawer-close').addEventListener('click', () => {
  document.querySelector('.drawer').classList.remove('drawer-open');
  document.querySelector('.drawer-backdrop').classList.remove('drawer-backdrop-show');
});

API Reference

Drawer Classes

Class Description
.drawer Base drawer container (required)
.drawer-left Left-side drawer (default)
.drawer-right Right-side drawer
.drawer-open Open state
.drawer-surface Surface background
.drawer-surface-container Surface container background
.drawer-surface-container-low Surface container low (default)
.drawer-surface-container-high Surface container high
.drawer-sm Small width (12rem)
.drawer-md Medium width (16rem, default)
.drawer-lg Large width (20rem)
.drawer-xl Extra large width (24rem)
.drawer-rail Rail variant (icon-only, 5rem)
.drawer-permanent Permanent drawer (always visible)

Content Classes

Class Description
.drawer-header Header section
.drawer-title Title text
.drawer-body Main content area
.drawer-footer Footer section
.drawer-close Close button
.drawer-label Section label
.drawer-divider Horizontal divider
.drawer-backdrop Backdrop overlay
.drawer-backdrop-show Show backdrop

Item Classes

Class Description
.drawer-item Navigation item
.drawer-item-active Active item (primary)
.drawer-item-active-primary Active with primary color
.drawer-item-active-secondary Active with secondary color
.drawer-item-active-tertiary Active with tertiary color
.drawer-item-icon Icon container
.drawer-item-badge Badge indicator
.drawer-item-nested First level nested item
.drawer-item-nested-2 Second level nested item

See Also