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 = ''">×</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 (×). 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>
2. Trigger from a button or link
<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 = ''">×</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 = ''">×</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 = ''">×</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 |