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.

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

ClassDescription
.drawerBase drawer container (required)
.drawer-leftLeft-side drawer (default)
.drawer-rightRight-side drawer
.drawer-openOpen state
.drawer-surfaceSurface background
.drawer-surface-containerSurface container background
.drawer-surface-container-lowSurface container low (default)
.drawer-surface-container-highSurface container high
.drawer-smSmall width (12rem)
.drawer-mdMedium width (16rem, default)
.drawer-lgLarge width (20rem)
.drawer-xlExtra large width (24rem)
.drawer-railRail variant (icon-only, 5rem)
.drawer-permanentPermanent drawer (always visible)

Content Classes

ClassDescription
.drawer-headerHeader section
.drawer-titleTitle text
.drawer-bodyMain content area
.drawer-footerFooter section
.drawer-closeClose button
.drawer-labelSection label
.drawer-dividerHorizontal divider
.drawer-backdropBackdrop overlay
.drawer-backdrop-showShow backdrop

Item Classes

ClassDescription
.drawer-itemNavigation item
.drawer-item-activeActive item (primary)
.drawer-item-active-primaryActive with primary color
.drawer-item-active-secondaryActive with secondary color
.drawer-item-active-tertiaryActive with tertiary color
.drawer-item-iconIcon container
.drawer-item-badgeBadge indicator
.drawer-item-nestedFirst level nested item
.drawer-item-nested-2Second level nested item

See Also