UI reference

Drawer

Platform-level slide-out panel for displaying contextual content — task details, settings, forms, previews, or any view that shouldn't navigate away from the current page.

CSS: public/design.css Target container: App-defined (e.g., <div id="task-panel-root"></div>) Interaction: HTMX loads HTML into the target; clicking the overlay or close button clears it.


Structure

Every drawer follows the same three-part structure:

<!-- 1. Overlay — translucent blur backdrop, click to dismiss -->
<div class="drawer-overlay" onclick="document.getElementById('my-panel').innerHTML = ''"></div>

<!-- 2. Drawer — slides in from the right -->
<div class="drawer">

  <!-- 3a. Header — sticky, always visible -->
  <div class="drawer-header">
    <h2 class="drawer-title">Panel Title</h2>
    <button type="button" class="drawer-close"
            onclick="document.getElementById('my-panel').innerHTML = ''">&times;</button>
  </div>

  <!-- 3b. Body — scrollable content area (hidden scrollbar) -->
  <div class="drawer-body">
    <!-- Your content here -->
  </div>

</div>

The overlay and drawer are siblings — both injected into a single target container via HTMX. Clearing the container's innerHTML dismisses everything.


Classes

Class Element Purpose
.drawer-overlay div Fixed full-screen backdrop with blur and dark tint. cursor: pointer — click to close.
.drawer div Fixed right-side panel. Slides in with animation. Flex column layout.
.drawer-lg .drawer Width variant: 70vw instead of 50vw.
.drawer-header div Sticky header with title and close button. Stays pinned at top while body scrolls.
.drawer-title h2 Panel heading.
.drawer-close button Close button (&times;). Hover state included.
.drawer-body div Scrollable content area. Scrollbar is hidden across all browsers.

Sizing

Variant Width Min-width Use case
.drawer (default) 50vw 480px Most panels — task detail, settings, previews
.drawer-lg 70vw 480px Content-heavy panels — editors, comparisons
Mobile (<768px) 100vw Full-screen on mobile automatically

All drawers occupy at least 50% of the viewport. There is no small variant by design — panels below 50% feel cramped and are harder to read.


Z-index

Layer Z-index
.drawer-overlay 1040
.drawer 1050

These match the form drawer system (form-backdrop / form-drawer) so drawers and forms stack at the same level.


Usage with HTMX

1. Define a target container in your template

<!-- At the bottom of your page template -->
<div id="my-panel-root"></div>
<button type="button"
        hx-get="/partials/thing/123"
        hx-target="#my-panel-root"
        hx-swap="innerHTML">
  Open Details
</button>

3. Return a drawer partial from your handler

async def thing_detail(req: Request, res: Response, ctx: Context):
    thing = get_thing(req.params["id"])
    await res.render("partials/thing_detail.html", thing=thing)
<!-- partials/thing_detail.html -->
<div class="drawer-overlay" onclick="document.getElementById('my-panel-root').innerHTML = ''"></div>

<div class="drawer">
  <div class="drawer-header">
    <h2 class="drawer-title">{{ thing.name }}</h2>
    <button type="button" class="drawer-close"
            onclick="document.getElementById('my-panel-root').innerHTML = ''">&times;</button>
  </div>
  <div class="drawer-body">
    <p>{{ thing.description }}</p>
  </div>
</div>

Usage with Alpine.js

For drawers that need client-side state (tabs, toggles, local form handling):

<div class="drawer" x-data="{ view: 'detail' }">
  <div class="drawer-header">
    <div class="d-flex align-items-center gap-2">
      <h2 class="drawer-title">Item</h2>
      <button type="button" class="some-toggle"
              :class="{ 'active': view === 'edit' }"
              @click="view = view === 'edit' ? 'detail' : 'edit'">
        Edit
      </button>
    </div>
    <button type="button" class="drawer-close"
            onclick="document.getElementById('my-panel-root').innerHTML = ''">&times;</button>
  </div>

  <div class="drawer-body" x-show="view === 'detail'">
    <!-- read-only content -->
  </div>

  <div class="drawer-body" x-show="view === 'edit'" x-cloak>
    <!-- editable content -->
  </div>
</div>

Dismissal

The drawer is dismissed by clearing the target container. Three common patterns:

<!-- 1. Overlay click (always include this) -->
<div class="drawer-overlay" onclick="document.getElementById('target').innerHTML = ''"></div>

<!-- 2. Close button in header (always include this) -->
<button type="button" class="drawer-close"
        onclick="document.getElementById('target').innerHTML = ''">&times;</button>

<!-- 3. Escape key (optional, add to the drawer element) -->
<div class="drawer" @keydown.escape.window="document.getElementById('target').innerHTML = ''">

Relationship to Form Drawer

The form drawer (form-backdrop, form-drawer, etc.) is a separate, higher-level system built specifically for the core.forms framework. It includes form submission logic, validation display, and field rendering.

Use this drawer (.drawer) when you need a general-purpose panel for any content. Use the form drawer when rendering a core.forms.Form via build_form_context().

Need System
Display task details, settings, previews .drawer
Render a core.forms.Form with validation form-drawer via forms/panel.html
Custom content that happens to have inputs .drawer with your own form handling