HyDrawer
Usage examples on this page are written for Lit / plain HTML (
<hy-drawer>). The same component ships asHyDrawerin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
A drawer component that slides in from the edge of the screen.
Key Features
- Slides in from any edge (start, end, top, bottom)
- Uses logical properties for RTL support
- Four variants: temporary (modal or non-modal overlay), persistent, permanent, and responsive (docks inline when its region is wide, an overlay sheet when narrow)
- Mini/collapsed mode with icon-only display
- Configurable size presets or custom values
- Focus trapping and management (temporary variant)
- Cancelable close events
Variants
- Temporary (default): Overlay drawer, modal by default (scrim + focus trap); set
modal={false}for a non-modal floating panel (Popover API, no scrim, page stays interactive) - Persistent: Toggleable drawer without overlay, content shifts aside
- Permanent: Always visible, integrated into layout
- Responsive: One declaration, one content source — morphs between a docked inline rail (region ≥
docked-at) and an overlay sheet with a self-trigger (region <docked-at). A ResizeObserver on the parent region drives the switch and reflectsdata-mode="docked|overlay"so the surrounding layout can key on the active mode at the same breakpoint.
Mini Mode
When mini is enabled, the drawer collapses to show only icons. Use expand-on-hover to expand the mini drawer when hovered.
Dialog or landmark
Only a temporary drawer is a dialog. The rails — persistent, permanent and a docked responsive drawer — sit in the page flow beside the content, so they render a role="complementary" landmark (a <div>, named by the label like the dialog) instead of a <dialog>: a screen reader lists them with the page's other regions, never as a dialog that was opened. A closed persistent rail is hidden. The React twin renders the same roles, and renders the rails in flow, never portaled.
Native Dialog Approach
The temporary variant leverages the browser's native <dialog> element which provides:
- Top Layer Rendering: Drawer appears above all other content automatically
- Inert Background: Modal variant makes outside content non-interactive
- Backdrop Support: Native
::backdroppseudo-element for overlay styling - Escape Key Handling: Built-in keyboard dismissal with the
cancelevent - Accessibility: Implicit
role="dialog"andaria-modal="true"
Initial Focus
On show, the drawer follows the same [autofocus] discovery contract as hy-dialog: it queries for a slotted descendant marked with autofocus, emits a cancelable initial-focus event, and (if not prevented) focuses the resolved target. When no marker is set, focus falls back to the close button (or collapse trigger) so keyboard users land somewhere sensible. The public focusSettled promise resolves once this sequence has settled (deterministic await for tests).
Accessibility: built in
- Native dialog accessibility for temporary drawers (role="dialog", aria-modal based on
modal) - Rails (persistent, permanent, docked responsive) are
role="complementary"landmarks - Configurable aria-labelledby and aria-label
- Configurable close button label for internationalization (close-label)
- Configurable collapse trigger label for internationalization (collapse-label)
- Focus restoration to trigger element on close
- Autofocus discovery via
[autofocus]marker; cancelable viainitial-focusevent - Escape key dismissal with cancelable event
- Backdrop click dismissal
- Body scroll lock when open (temporary variant)
- Focus management (focus not obscured)
- Screen reader announcements on open/close
Accessibility: your part
- Set
labelto what the drawer holds ("Filters"). The default is "Drawer", and it names the dialog or landmark even withno-header.
When to use
- A task done beside the page people keep in view: filters, details of a selected row, a settings panel.
- A form or content too long for a popover that should not leave the page.
- Navigation that slides in on small screens, with
variant="responsive".
Do
- Open details from the end edge and navigation from the start edge.
- Pick the
sizethat fits its content without crowding the page beside it.
When not to use
- A decision people must make before going on:
hy-dialog. - A short menu or a couple of fields beside a trigger:
hy-popover. - The main task itself, or content that needs most of the screen: a page of its own.
Examples
Basic Usage (Temporary/Modal)
<hy-drawer label="Settings" open>
<p>Configure your preferences here.</p>
</hy-drawer>Self-managing trigger (no open wiring)
<hy-drawer label="Filters" placement="end">
<hy-button slot="trigger">Filters</hy-button>
<p>Filter facets go here.</p>
</hy-drawer>Responsive morph (docked rail ↔ overlay sheet)
<!-- Wide region: docked inline rail. Narrow region: a "Filters" trigger
opens an overlay sheet. Same fragment, same single content source. -->
<hy-drawer variant="responsive" docked-at="md" placement="start" label="Filters">
<hy-button slot="trigger">Filters</hy-button>
<p>Filter facets — authored once.</p>
</hy-drawer>Persistent Drawer (Sidebar)
<hy-drawer variant="persistent" placement="start" open>
<nav>
<a href="/home">Home</a>
<a href="/settings">Settings</a>
</nav>
</hy-drawer>Permanent Drawer (Always Visible)
<hy-drawer variant="permanent" placement="start">
<nav>Navigation always visible</nav>
</hy-drawer>Mini Drawer with Expand on Hover
<hy-drawer variant="permanent" placement="start" mini expand-on-hover>
<nav>
<hy-icon name="home"></hy-icon> <span>Home</span> <hy-icon name="settings"></hy-icon>
<span>Settings</span>
</nav>
</hy-drawer>Collapsible Sidebar with Collapse Trigger
<hy-drawer variant="permanent" placement="start" collapse-trigger>
<nav>
<hy-icon name="home"></hy-icon> <span>Home</span> <hy-icon name="settings"></hy-icon>
<span>Settings</span>
</nav>
</hy-drawer>With Placement
<hy-drawer label="Navigation" placement="start" open>
<nav>Navigation items here</nav>
</hy-drawer>With Footer Actions
<hy-drawer label="Edit Profile">
<form>Form content here</form>
<div slot="footer">
<hy-button variant="neutral">Cancel</hy-button>
<hy-button variant="brand">Save</hy-button>
</div>
</hy-drawer>Programmatic Control
<hy-button id="openBtn">Open Drawer</hy-button>
<hy-drawer id="myDrawer" label="Settings">
<p>Drawer content</p>
</hy-drawer>
<script>
const drawer = document.getElementById('myDrawer');
const openBtn = document.getElementById('openBtn');
openBtn.addEventListener('click', () => drawer.show());
// Listen for close requests (cancelable)
drawer.addEventListener('request-close', (e) => {
if (hasUnsavedChanges) {
e.preventDefault(); // Keep drawer open
showUnsavedWarning();
}
});
</script>Custom Size
<hy-drawer label="Wide Drawer" size="600px" placement="end">
<p>This drawer has a custom width.</p>
</hy-drawer>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
titleId | title-id | string | '' | Auto-generated unique identifier for ARIA relationships |
label | label | string | 'Drawer' | Accessible label for the drawer |
closeLabel | close-label | string | 'Close' | Accessible label for the close button. Use for internationalization. |
collapseLabel | collapse-label | string | 'Toggle sidebar' | Accessible label for the collapse trigger button. Use for internationalization. |
openedLabel | opened-label | string | '{label} drawer opened' | The announcement read when the drawer opens; {label} is the drawer's label. Use for internationalization. |
closedLabel | closed-label | string | '{label} drawer closed' | The announcement read when the drawer closes; {label} is the drawer's label. Use for internationalization. |
open | open | boolean | false | Whether the drawer is currently open |
dockedAt | docked-at | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Region width at/above which a responsive drawer docks inline (overlay sheet below it). Only meaningful when variant="responsive". |
placement | placement | 'start' | 'end' | 'top' | 'bottom' | 'end' | Position of the drawer |
size | size | 'small' | 'medium' | 'large' | 'medium' | small / medium / large, or any CSS length (e.g. 500px, 30vw) as an escape hatch. The content never sets the width: content wider than the drawer's column makes the body scroll sideways, and a development warning names it. |
variant | variant | 'temporary' | 'persistent' | 'permanent' | 'responsive' | 'temporary' | Drawer variant determines behavior: - temporary: Overlay drawer (default); modal by default, modal={false} for non-modal - persistent: Toggleable drawer without overlay - permanent: Always visible, cannot be closed - responsive: Docks inline as a rail when its region is at least docked-at wide; below that, a trigger opens it as an overlay sheet |
mini | mini | boolean | false | Whether the drawer is in mini/collapsed mode showing only icons |
expandOnHover | expand-on-hover | boolean | false | Whether mini drawer should expand on hover |
collapseTrigger | collapse-trigger | boolean | false | Whether to show a collapse/expand trigger button in the header. Only for persistent/permanent variants. |
noHeader | no-header | boolean | false | Whether to hide the default header |
modal | modal | boolean | true | Whether the drawer behaves as a modal. Only meaningful for the temporary variant (persistent / permanent ignore it). |
closeOnEscape | close-on-escape | boolean | true | Whether pressing Escape closes the drawer |
closeOnOverlayClick | close-on-overlay-click | boolean | true | Whether clicking the overlay closes the drawer |
returnFocus | return-focus | boolean | true | Whether to return focus to the trigger element on close |
destroyOnClose | destroy-on-close | boolean | false | Whether to remove content from DOM when closed |
contained | contained | boolean | false | Whether the drawer is contained within its parent element (the nearest positioned ancestor) instead of the viewport. The drawer never enters the top layer: it opens via <dialog>.show() regardless of modal, and body scroll is never locked. With modal, the scrim covers the parent (not the page), Escape still dismisses (via a document listener — show() fires no native cancel), and a JS focus trap supplies the Tab cycling that showModal() would otherwise provide. Without modal, there is no scrim: the parent region stays interactive beside the panel, Escape closes it (closeOnEscape), and it takes focus on open. A temporary contained drawer clips its own layer, so its panel, off the parent's edge while it slides, never gives the parent anything to scroll: the content beside it stays put when it opens, even in a parent that clips with overflow: hidden (which code can still scroll). |
for | for | string | '' | Id of an external trigger element that toggles the drawer. Fallback for the slotted trigger pattern when the trigger must live outside the drawer (e.g. SSR). Resolved by walking the shadow-host chain. The slotted trigger slot takes precedence when both are present. Most meaningful for the temporary variant; permanent drawers are always open. On a responsive drawer only a slotted trigger is hidden while docked — a for-bound one stays visible and does nothing there, so prefer the slot. |
focusSettled | — | Promise<void> | — | Resolves once the current open's initial-focus sequence has settled — discovery ran and focus was applied (marker, close button, or collapse trigger), or the sequence was skipped (non-modal drawers never steal focus; closed before the deferred work ran). Before the first open it is already resolved. |
Events
| Event | Detail | Description |
|---|---|---|
show | — | Fired when the drawer opens. Detail: { source: HyVisibilitySource } |
hide | — | Fired when the drawer closes (after animation). Detail: { source: HyVisibilitySource } |
request-close | — | Fired before closing; cancelable to prevent close. Detail: { source: HyVisibilitySource } |
initial-focus | — | Fired on show after the autofocus discovery runs. Cancelable: call event.preventDefault() to skip the default focus and apply your own. Detail: HyInitialFocusDetail |
collapse | — | Fired when the rail's effective state changes — mini toggled (the collapse trigger or a property write), or a mini + expand-on-hover rail widening and narrowing as the pointer or focus enters and leaves it. Not fired for the initial state. Detail: { collapsed: boolean } (DrawerCollapseDetail). A slotted hy-sidebar fires its own collapse, which bubbles through the drawer — check event.target === event.currentTarget before following it. |
Slots
| Slot | Description |
|---|---|
trigger | An element that toggles the drawer on click (self-managing, no open wiring required). ARIA (aria-controls, aria-expanded, and aria-haspopup="dialog" on a temporary drawer only — a rail is a landmark, not a popup) is applied automatically and focus returns to the trigger on close. A controlled open still wins when both are present. Most useful on the temporary variant. |
default | The main content displayed in the drawer body |
header-actions | Additional actions in the header (alongside close button) |
footer | Footer content, typically action buttons |
CSS Parts
| Part | Description |
|---|---|
base | The native <dialog> element of a temporary drawer; on a rail (persistent, permanent, docked responsive) the role="complementary" landmark <div> |
overlay | The backdrop overlay element |
panel | The drawer panel container |
header | The header section containing title and close button |
title | The title element (h2) when using default header |
header-actions | Container for header action buttons |
collapse-trigger | The collapse/expand trigger button in the header |
close-button | The close button in the header |
body | The scrollable content area |
footer | The footer section for action buttons |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-drawer-scrim-background-overlay-rest | var(--hy-background-overlay-rest) | Backdrop scrim color. |
--hy-drawer-background-surface-elevation-5 | var(--hy-background-surface-elevation-5) | Panel background color. (same as surface-base in light mode; lifts in dark mode for OLED-true-black visibility) |
--hy-drawer-foreground-default-rest | var(--hy-foreground-default-rest) | Panel text color |
--hy-drawer-stroke-sm | var(--hy-stroke-sm) | Border width (the one edge facing the content) |
--hy-drawer-border-elevated-rest | var(--hy-border-elevated-rest) | Border color (the one edge facing the content — the rim of the floating surface) |
--hy-drawer-padding-overlay-modal-md | var(--hy-padding-overlay-modal-md) | The panel inset (header top, footer bottom, every region's inline edges) |
--hy-drawer-gap-overlay-modal-lg | var(--hy-gap-overlay-modal-lg) | Gap between header, body and footer |
--hy-drawer-icon-size-md | var(--hy-icon-size-md) | Close glyph size (24px at standard density; the 48px target is kept) |
--hy-drawer-padding-overlay-modal-sm | var(--hy-padding-overlay-modal-sm) | The body's inset beside a scroll hairline (header / footer side) |
--hy-drawer-padding-overlay-modal-xs | var(--hy-padding-overlay-modal-xs) | Body padding in mini mode |
--hy-drawer-padding-layout-xs | var(--hy-padding-layout-xs) | Close/collapse button padding |
--hy-drawer-gap-overlay-modal-md | var(--hy-gap-overlay-modal-md) | Gap between header/footer items |
--hy-drawer-gap-overlay-modal-sm | var(--hy-gap-overlay-modal-sm) | Gap between header action buttons |
--hy-drawer-radius-action-md | var(--hy-radius-action-md) | Close/collapse button border radius |
--hy-drawer-surface-width-xs | var(--hy-surface-width-xs) | Width of mini/collapsed drawer |
--hy-drawer-surface-width-sm | var(--hy-surface-width-sm) | Panel width (or height for top/bottom) for size="small" |
--hy-drawer-surface-width-md | var(--hy-surface-width-md) | Panel width (or height for top/bottom) for size="medium" |
--hy-drawer-surface-width-lg | var(--hy-surface-width-lg) | Panel width (or height for top/bottom) for size="large" |
--hy-drawer-background-muted-hover | var(--hy-background-muted-hover) | Close/collapse button hover background |
--hy-drawer-overlay-shadow-color | var(--hy-overlay-shadow-color) | Scroll shadow color, shown while the body scrolls down |
--hy-drawer-background-overlay-strong | var(--hy-background-overlay-strong) | Forced-colors overlay background |
--hy-drawer-shadow-elevation-5 | var(--hy-shadow-elevation-5) | Box shadow for the drawer panel |
--hy-drawer-title-md-font-family | var(--hy-title-md-font-family) | Font family override for drawer title |
--hy-drawer-motion-transition-enter-duration | var(--hy-motion-transition-enter-duration) | Transition duration for the open animation |
--hy-drawer-motion-transition-enter-easing | var(--hy-motion-transition-enter-easing) | Transition easing for the open animation |
--hy-drawer-motion-transition-exit-duration | var(--hy-motion-transition-exit-duration) | Transition duration for the close animation |
--hy-drawer-motion-transition-exit-easing | var(--hy-motion-transition-exit-easing) | Transition easing for the close animation |
--hy-drawer-motion-transition-expand-duration | var(--hy-motion-transition-expand-duration) | Transition duration for the expand and collapse animation |
--hy-drawer-motion-transition-expand-easing | var(--hy-motion-transition-expand-easing) | Transition easing for the expand and collapse animation |
Methods
show()
Opens the drawer programmatically. Has no effect on permanent variant (always visible).
hide()
Closes the drawer programmatically. Emits a cancelable request-close event first. Has no effect on permanent variant (cannot be closed).
Parameters:
source- How the close was triggered