Skip to content

Drawer

hc-drawer styles a native <dialog> element as a panel that slides in from any edge of the viewport. The native dialog handles focus trapping, Escape-to-close, and the ::backdrop layer for free; HC adds the edge positioning, the slide-in / slide-out animation, and (via installDrawer) the backdrop-click-to-close affordance users expect from a slide panel.

PrimitiveRequired version
HTML <dialog> + showModal()All evergreen browsers
CSS @starting-style + transition-behavior: allow-discreteChrome 117+, Firefox 129+, Safari 17.5+

The slide animation degrades gracefully — older browsers without @starting-style snap to the final position without the entry transition, but the drawer is still functional.

Settings

Form fields, settings, anything.

import { installDrawer } from '@hypermedia-components/core';
installDrawer();

installDrawer() is idempotent and returns an uninstaller. The zero-config @hypermedia-components/core/behaviors entry auto-installs it and picks up content swapped in by htmx.

data-side accepts right (default), left, top, and bottom. Right/left drawers fill the viewport height and cap their width at --hc-drawer-side-max-width; top/bottom drawers fill the viewport width and cap their height at --hc-drawer-vert-max-height.

Left drawer

Slides in from the left edge.

Top drawer

Slides in from the top edge.

Bottom drawer

Slides in from the bottom edge.

Three idiomatic patterns, in order of preference:

A button inside a form whose method="dialog" closes the dialog on submit. Native, accessible, declarative.

<form method="dialog">
<button class="hc-button" type="submit">Cancel</button>
</form>

Clicking outside the drawer panel (on the ::backdrop area) closes the dialog. The behavior detects this via event.target === dialog.

Drag the panel toward its anchored edge to dismiss it. The axis follows data-side (right / left → horizontal, top / bottom → vertical); past ~40% of the panel size, or with a quick flick, the drawer slides out and closes — release short of that and it snaps back. Only the outward direction moves (an inward drag is clamped, so there’s no rubber-banding and prefers-reduced-motion needs no special case).

The gesture is grabbed from the panel chrome — the __header / __footer — never the scrollable __body or an interactive control, so content scrolling and buttons keep working. Pointer-Events based, so it works for mouse, touch, and pen.

document.getElementById('settings-drawer').close();

Useful from htmx response handlers — pair with installCloseDialog for the htmx-success-closes-the-dialog pattern.

Open the drawer in response to a server fetch and let htmx swap content into it:

<button class="hc-button"
data-hx-get="/users/42/edit"
data-hx-target="#user-drawer .hc-drawer__body"
data-hx-swap="innerHTML"
onclick="document.getElementById('user-drawer').showModal()">
Edit user
</button>
<dialog class="hc-drawer" id="user-drawer" data-side="right">
<header class="hc-drawer__header">
<h2 class="hc-drawer__title">Edit user</h2>
<form method="dialog">
<button class="hc-button" data-variant="ghost" data-size="sm"
type="submit" aria-label="Close">×</button>
</form>
</header>
<div class="hc-drawer__body">
<hc-spinner></hc-spinner>
</div>
</dialog>

For close-on-success after a form save:

<form
data-hx-post="/users/42"
data-hx-target="this"
data-hc-close-dialog-on-success>
</form>

The bundled installCloseDialog behavior closes the enclosing <dialog> on a successful htmx response. Same pattern as hc-dialog.

  • The native <dialog> already exposes the right role, focus trap, and Escape semantics. Don’t add role="dialog" or aria-modal="true" — they are redundant and can confuse some screen readers.
  • Always include a focusable element inside the drawer so focus trapping has something to land on. A close button in the header or a primary action in the footer is plenty.
  • Provide a visible close affordance. The <form method="dialog"> pattern shown above pairs the visible ”×” button with a built-in keyboard activation path (Enter / Space on the button).
  • Slide animations respect prefers-reduced-motion: reduce — transition duration drops to 0 ms when the user has opted out.

The drawer ships with one visual style intentionally — the surface follows the same conventions as hc-dialog, so applying the same theming tokens (light / dark / color) Just Works. Variants like data-variant="error" for destructive flows can be modelled with content (a red header, an error alert at the top) rather than the container chrome.

Component tokens (in component.tokens.json):

Token pathPurpose
drawer.bg / fg / borderSurface colors.
drawer.side-max-widthCap on the right / left drawer width.
drawer.vert-max-heightCap on the top / bottom drawer height.
drawer.paddingInner padding for header / body / footer.
drawer.gapHeader / footer flex gap.
drawer.backdrop::backdrop background.
drawer.durationSlide animation duration.
Show the generated CSS variables
--hc-drawer-bg | -fg | -border
--hc-drawer-side-max-width | -vert-max-height
--hc-drawer-padding | -gap
--hc-drawer-backdrop | -duration
  • Dialog — centred modal sibling.
  • Popover — for transient floating content.