mitgliederverwaltung/docs/daisyui-drawer-pattern.md
Simon 16ca4efc03
Some checks failed
continuous-integration/drone/push Build is failing
feat: implement standard-compliant sidebar with comprehensive tests
Implement a new sidebar component based on DaisyUI Drawer pattern without
custom CSS variants. The sidebar supports desktop (expanded/collapsed states)
and mobile (overlay drawer) with full accessibility compliance.

Sidebar Implementation:
- Refactor sidebar component with sidebar_header, menu_item, menu_group,
  sidebar_footer sub-components
- Add logo (mila.svg) with size-8 (32px) always visible
- Implement toggle button with icon swap (chevron-left/right) for desktop
- Add nested menu support with details/summary (expanded) and dropdown
  (collapsed) patterns
- Implement footer with language selector (expanded-only), theme toggle,
  and user menu with avatar
- Update layouts.ex to use drawer pattern with data-sidebar-expanded
  attribute for state management

CSS & JavaScript:
- Add CSS styles for sidebar state management via data-attribute selectors
- Implement SidebarState JavaScript hook for localStorage persistence
- Add smooth width transitions (w-64 ↔ w-16) for desktop collapsed state
- Add CSS classes for expanded-only, menu-label, and icon visibility

Documentation:
- Add sidebar-analysis-current-state.md: Analysis of current implementation
- Add sidebar-requirements-v2.md: Complete specification for new sidebar
- Add daisyui-drawer-pattern.md: DaisyUI pattern documentation
- Add umsetzung-sidebar.md: Step-by-step implementation guide

Testing:
- Add comprehensive component tests for all sidebar sub-components
- Add integration tests for sidebar state management and mobile drawer
- Extend accessibility tests (ARIA labels, roles, keyboard navigation)
- Add regression tests for duplicate IDs, hover effects, and tooltips
- Ensure full test coverage per specification requirements
2025-12-18 16:36:16 +01:00

15 KiB

DaisyUI Drawer Pattern - Standard Implementation

This document describes the standard DaisyUI drawer pattern for implementing responsive sidebars. It covers mobile overlay drawers, desktop persistent sidebars, and their combination.

Core Concept

DaisyUI's drawer component uses a checkbox-based toggle mechanism combined with CSS to create accessible, responsive sidebars without custom JavaScript.

Key Components

  1. drawer - Container element
  2. drawer-toggle - Hidden checkbox that controls open/close state
  3. drawer-content - Main content area
  4. drawer-side - Sidebar content (menu, navigation)
  5. drawer-overlay - Optional overlay for mobile (closes drawer on click)

HTML Structure

<div class="drawer">
  <!-- Hidden checkbox controls the drawer state -->
  <input id="my-drawer" type="checkbox" class="drawer-toggle" />
  
  <!-- Main content area -->
  <div class="drawer-content">
    <!-- Page content goes here -->
    <label for="my-drawer" class="btn btn-primary">Open drawer</label>
  </div>
  
  <!-- Sidebar content -->
  <div class="drawer-side">
    <label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
    <ul class="menu p-4 w-80 min-h-full bg-base-200 text-base-content">
      <!-- Sidebar content goes here -->
      <li><a>Sidebar Item 1</a></li>
      <li><a>Sidebar Item 2</a></li>
    </ul>
  </div>
</div>

How drawer-toggle Works

Mechanism

The drawer-toggle is a hidden checkbox that serves as the state controller:

<input id="my-drawer" type="checkbox" class="drawer-toggle" />

Toggle Behavior

  1. Label Connection: Any <label for="my-drawer"> element can toggle the drawer
  2. Checkbox State:
    • checked → drawer is open
    • unchecked → drawer is closed
  3. CSS Targeting: DaisyUI uses CSS sibling selectors to show/hide the drawer based on checkbox state
  4. Accessibility: Native checkbox provides keyboard accessibility (Space/Enter to toggle)

Toggle Examples

<!-- Button to open drawer -->
<label for="my-drawer" class="btn btn-primary drawer-button">
  Open Menu
</label>

<!-- Close button inside drawer -->
<label for="my-drawer" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></label>

<!-- Overlay to close (click outside) -->
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>

Mobile Drawer (Overlay)

Characteristics

  • Drawer slides in from the side (usually left)
  • Overlays the main content
  • Dark overlay (drawer-overlay) behind drawer
  • Clicking overlay closes the drawer
  • Typically used on mobile/tablet screens

Implementation

<div class="drawer">
  <input id="mobile-drawer" type="checkbox" class="drawer-toggle" />
  
  <div class="drawer-content">
    <!-- Toggle button in header -->
    <div class="navbar bg-base-100">
      <div class="flex-none">
        <label for="mobile-drawer" class="btn btn-square btn-ghost">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
          </svg>
        </label>
      </div>
      <div class="flex-1">
        <a class="btn btn-ghost text-xl">My App</a>
      </div>
    </div>
    
    <!-- Main content -->
    <div class="p-4">
      <h1>Main Content</h1>
    </div>
  </div>
  
  <div class="drawer-side">
    <!-- Overlay - clicking it closes the drawer -->
    <label for="mobile-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
    
    <!-- Sidebar menu -->
    <ul class="menu p-4 w-80 min-h-full bg-base-200">
      <li><a>Home</a></li>
      <li><a>About</a></li>
      <li><a>Contact</a></li>
    </ul>
  </div>
</div>

Styling Notes

  • Width: Default w-80 (320px), adjust with Tailwind width utilities
  • Background: Use DaisyUI color classes like bg-base-200
  • Height: Always use min-h-full to ensure full height
  • Padding: Add p-4 or similar for inner spacing

Desktop Sidebar (Persistent)

Characteristics

  • Always visible (no overlay)
  • Does not overlay main content
  • Main content adjusts to sidebar width
  • No toggle button needed
  • Used on desktop screens

Implementation with drawer-open

<div class="drawer lg:drawer-open">
  <input id="desktop-drawer" type="checkbox" class="drawer-toggle" />
  
  <div class="drawer-content">
    <!-- Main content -->
    <div class="p-4">
      <h1>Main Content</h1>
      <p>The sidebar is always visible on desktop (lg and above)</p>
    </div>
  </div>
  
  <div class="drawer-side">
    <!-- No overlay needed for persistent sidebar -->
    <label for="desktop-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
    
    <!-- Sidebar menu -->
    <ul class="menu p-4 w-80 min-h-full bg-base-200">
      <li><a>Dashboard</a></li>
      <li><a>Settings</a></li>
      <li><a>Profile</a></li>
    </ul>
  </div>
</div>

How drawer-open Works

The drawer-open class forces the drawer to be permanently open:

<div class="drawer drawer-open">
  • Drawer is always visible
  • Cannot be toggled closed
  • drawer-toggle checkbox is ignored
  • drawer-overlay is not shown
  • Main content automatically shifts to accommodate sidebar width

Responsive Usage

Use Tailwind breakpoint modifiers for responsive behavior:

<!-- Open on large screens and above -->
<div class="drawer lg:drawer-open">

<!-- Open on medium screens and above -->
<div class="drawer md:drawer-open">

<!-- Open on extra-large screens and above -->
<div class="drawer xl:drawer-open">

This is the most common pattern for responsive applications: mobile overlay + desktop persistent.

Complete Implementation

<div class="drawer lg:drawer-open">
  <!-- Checkbox for mobile toggle -->
  <input id="app-drawer" type="checkbox" class="drawer-toggle" />
  
  <div class="drawer-content flex flex-col">
    <!-- Navbar with mobile menu button -->
    <div class="navbar bg-base-100 lg:hidden">
      <div class="flex-none">
        <label for="app-drawer" class="btn btn-square btn-ghost">
          <!-- Hamburger icon -->
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
          </svg>
        </label>
      </div>
      <div class="flex-1">
        <a class="btn btn-ghost text-xl">My App</a>
      </div>
    </div>
    
    <!-- Main content -->
    <div class="flex-1 p-6">
      <h1 class="text-3xl font-bold mb-4">Welcome</h1>
      <p>This is the main content area.</p>
      <p>On mobile (< lg): sidebar is hidden, hamburger menu visible</p>
      <p>On desktop (≥ lg): sidebar is persistent, hamburger menu hidden</p>
    </div>
  </div>
  
  <div class="drawer-side">
    <!-- Overlay only shows on mobile -->
    <label for="app-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
    
    <!-- Sidebar navigation -->
    <aside class="bg-base-200 w-80 min-h-full">
      <!-- Logo/Header area -->
      <div class="p-4 font-bold text-xl border-b border-base-300">
        My App Logo
      </div>
      
      <!-- Navigation menu -->
      <ul class="menu p-4">
        <li><a>
          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
          </svg>
          Dashboard
        </a></li>
        <li><a>
          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
          </svg>
          Documents
        </a></li>
        <li><a>
          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
          </svg>
          Settings
        </a></li>
      </ul>
    </aside>
  </div>
</div>

Behavior Breakdown

On Mobile (< 1024px / < lg)

  1. Sidebar is hidden by default
  2. Hamburger button visible in navbar
  3. Clicking hamburger opens sidebar as overlay
  4. Clicking overlay or close button closes sidebar
  5. Sidebar slides in from left with animation

On Desktop (≥ 1024px / ≥ lg)

  1. lg:drawer-open keeps sidebar permanently visible
  2. Hamburger button hidden via lg:hidden
  3. Sidebar takes up fixed width (320px)
  4. Main content area adjusts automatically
  5. No overlay, no toggle needed

Tailwind Breakpoints Reference

/* Default (mobile-first) */
/* < 640px */

sm: /* ≥ 640px */
md: /* ≥ 768px */
lg: /* ≥ 1024px */  Common desktop breakpoint
xl: /* ≥ 1280px */
2xl: /* ≥ 1536px */

Key Classes Summary

Class Purpose
drawer Main container
drawer-toggle Hidden checkbox for state control
drawer-content Main content area
drawer-side Sidebar container
drawer-overlay Clickable overlay (closes drawer)
drawer-open Forces drawer to stay open
drawer-end Positions drawer on the right side
lg:drawer-open Opens drawer on large screens only

Positioning Variants

Left Side Drawer (Default)

<div class="drawer">
  <!-- Drawer appears on the left -->
</div>

Right Side Drawer

<div class="drawer drawer-end">
  <!-- Drawer appears on the right -->
</div>

Best Practices

1. Accessibility

  • Always include aria-label on overlay: <label for="drawer" aria-label="close sidebar" class="drawer-overlay"></label>
  • Use semantic HTML (<nav>, <aside>)
  • Ensure keyboard navigation works (native checkbox provides this)

2. Responsive Design

  • Use lg:drawer-open for desktop persistence
  • Hide mobile toggle button on desktop: lg:hidden
  • Adjust sidebar width for mobile if needed: w-64 md:w-80

3. Performance

  • DaisyUI drawer is pure CSS (no JavaScript needed)
  • Animations are handled by CSS transitions
  • No performance overhead

4. Styling

  • Use DaisyUI theme colors: bg-base-200, text-base-content
  • Maintain consistent spacing: p-4, gap-2
  • Use DaisyUI menu component for navigation: <ul class="menu">

5. Content Structure

<div class="drawer-content flex flex-col">
  <!-- Navbar (if needed) -->
  <div class="navbar">...</div>
  
  <!-- Main content with flex-1 to fill space -->
  <div class="flex-1 p-6">
    <!-- Your content -->
  </div>
  
  <!-- Footer (if needed) -->
  <footer>...</footer>
</div>

Common Patterns

Pattern 1: Drawer with Close Button

<div class="drawer-side">
  <label for="drawer" class="drawer-overlay"></label>
  <aside class="bg-base-200 w-80 min-h-full relative">
    <!-- Close button (mobile only) -->
    <label for="drawer" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2 lg:hidden"></label>
    
    <!-- Sidebar content -->
    <ul class="menu p-4 pt-12">
      <li><a>Item 1</a></li>
    </ul>
  </aside>
</div>

Pattern 2: Drawer with User Profile

<aside class="bg-base-200 w-80 min-h-full flex flex-col">
  <!-- Logo -->
  <div class="p-4 font-bold text-xl">My App</div>
  
  <!-- Navigation (flex-1 to push footer down) -->
  <ul class="menu flex-1 p-4">
    <li><a>Dashboard</a></li>
    <li><a>Settings</a></li>
  </ul>
  
  <!-- User profile footer -->
  <div class="p-4 border-t border-base-300">
    <div class="flex items-center gap-2">
      <div class="avatar">
        <div class="w-10 rounded-full">
          <img src="/avatar.jpg" alt="User" />
        </div>
      </div>
      <div>
        <div class="font-semibold">John Doe</div>
        <div class="text-sm opacity-70">john@example.com</div>
      </div>
    </div>
  </div>
</aside>

Pattern 3: Nested Menu with Submenu

<ul class="menu p-4 w-80 min-h-full bg-base-200">
  <li><a>Dashboard</a></li>
  
  <!-- Submenu -->
  <li>
    <details>
      <summary>Products</summary>
      <ul>
        <li><a>Electronics</a></li>
        <li><a>Clothing</a></li>
        <li><a>Books</a></li>
      </ul>
    </details>
  </li>
  
  <li><a>Settings</a></li>
</ul>

Troubleshooting

Issue: Drawer doesn't open on mobile

Solution: Check that:

  1. Checkbox id matches label for attribute
  2. Checkbox has class drawer-toggle
  3. You're not using drawer-open on mobile breakpoints

Issue: Drawer overlaps content on desktop

Solution:

  • Remove drawer-open or use responsive variant lg:drawer-open
  • Ensure you want overlay behavior, not persistent sidebar

Issue: Overlay not clickable

Solution:

  • Ensure overlay label has correct for attribute
  • Check that overlay is not behind other elements (z-index)

Issue: Content jumps when drawer opens

Solution:

  • Add flex flex-col to drawer-content
  • Ensure drawer-side width is fixed (e.g., w-80)

Migration from Custom Solutions

If migrating from a custom sidebar implementation:

Replace custom JavaScript

Before:

function toggleDrawer() {
  document.getElementById('sidebar').classList.toggle('open');
}

After:

<input id="drawer" type="checkbox" class="drawer-toggle" />
<label for="drawer">Toggle</label>

Replace custom CSS

Before:

.sidebar {
  transform: translateX(-100%);
  transition: transform 0.3s;
}
.sidebar.open {
  transform: translateX(0);
}

After:

<div class="drawer">
  <!-- DaisyUI handles all transitions -->
</div>

Replace media query logic

Before:

@media (min-width: 1024px) {
  .sidebar { display: block; }
  .toggle-button { display: none; }
}

After:

<div class="drawer lg:drawer-open">
  <label for="drawer" class="lg:hidden">Toggle</label>
</div>

Summary

The DaisyUI drawer pattern provides:

Zero JavaScript - Pure CSS solution Accessible - Built-in keyboard support via checkbox Responsive - Easy mobile/desktop variants with Tailwind Themeable - Uses DaisyUI theme colors Flexible - Supports left/right positioning Standard - No custom CSS needed

Recommended approach: Use lg:drawer-open for desktop with hidden mobile toggle for best responsive experience.