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
- Three variants: temporary (modal), persistent, permanent
- 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.
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). See .claude/rules/focus.md for the contract.
Accessibility Features
- Native dialog accessibility (role="dialog", aria-modal based on variant)
- 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
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. |
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 | DrawerSize | 'medium' | Size preset or custom CSS value |
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 |
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. |
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. |
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 |
Slots
| Slot | Description |
|---|---|
trigger | An element that toggles the drawer on click (self-managing, no open wiring required). ARIA (aria-haspopup, aria-controls, aria-expanded) 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 | Custom header content that replaces the default title. |
header-actions | Additional actions in the header (alongside close button) |
footer | Footer content, typically action buttons |
toggle-icon | Custom collapse/expand icon inside the collapse trigger (overrides the default chevron) |
close-icon | Custom close icon inside the close button (overrides the default close icon) |
CSS Parts
| Part | Description |
|---|---|
base | The native <dialog> element |
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
| Property | Description |
|---|---|
--hy-drawer-overlay-background | Backdrop overlay background color (kept for HC fallbacks) |
--hy-drawer-scrim | Backdrop scrim color. Defaults to --hy-background-overlay-rest. |
--hy-drawer-surface | Panel background color. Default: --hy-background-surface-elevation-5 (same as surface-base in light mode; lifts in dark mode for OLED-true-black visibility) |
--hy-drawer-foreground | Panel text color |
--hy-drawer-stroke | Border width |
--hy-drawer-border | Border color |
--hy-drawer-padding | The panel inset (header top, footer bottom, every region's inline edges) |
--hy-drawer-gap-regions | Gap between header, body and footer |
--hy-drawer-close-icon-size | Close glyph size (default --hy-icon-size-md, 24px at standard density; the 48px target is kept) |
--hy-drawer-padding-mini | Body padding in mini mode |
--hy-drawer-button-padding | Close/collapse button padding |
--hy-drawer-gap | Gap between header/footer items |
--hy-drawer-gap-actions | Gap between header action buttons |
--hy-drawer-button-radius | Close/collapse button border radius |
--hy-drawer-mini-width | Width of mini/collapsed drawer |
--hy-drawer-size-sm | Panel width (or height for top/bottom) for size="small" (default: --hy-surface-width-sm) |
--hy-drawer-size-md | Panel width (or height for top/bottom) for size="medium" (default: --hy-surface-width-md) |
--hy-drawer-size-lg | Panel width (or height for top/bottom) for size="large" (default: --hy-surface-width-lg) |
--hy-drawer-button-hover-bg | Close/collapse button hover background |
--hy-drawer-body-shadow | Scroll shadow color for body overflow |
--hy-drawer-background-overlay-strong | Forced-colors overlay background |
--hy-drawer-shadow | Box shadow for the drawer panel |
--hy-drawer-font-family | Font family override for drawer title |
--hy-drawer-enter-duration | Transition duration for the open and close slide animation |
--hy-drawer-enter-easing | Transition easing for the open and close slide animation |
--hy-drawer-expand-duration | Transition duration for the expand and collapse animation |
--hy-drawer-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