Skip to content

HyDrawer

Usage examples on this page are written for Lit / plain HTML (<hy-drawer>). The same component ships as HyDrawer in @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 reflects data-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 ::backdrop pseudo-element for overlay styling
  • Escape Key Handling: Built-in keyboard dismissal with the cancel event
  • Accessibility: Implicit role="dialog" and aria-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 via initial-focus event
  • 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)

html
<hy-drawer label="Settings" open>
  <p>Configure your preferences here.</p>
</hy-drawer>

Self-managing trigger (no open wiring)

html
<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)

html
<!-- 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)

html
<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)

html
<hy-drawer variant="permanent" placement="start">
  <nav>Navigation always visible</nav>
</hy-drawer>

Mini Drawer with Expand on Hover

html
<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

html
<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

html
<hy-drawer label="Navigation" placement="start" open>
  <nav>Navigation items here</nav>
</hy-drawer>
html
<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

javascript
<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

html
<hy-drawer label="Wide Drawer" size="600px" placement="end">
  <p>This drawer has a custom width.</p>
</hy-drawer>

API

Properties

PropertyAttributeTypeDefaultDescription
titleIdtitle-idstring''Auto-generated unique identifier for ARIA relationships
labellabelstring'Drawer'Accessible label for the drawer
closeLabelclose-labelstring'Close'Accessible label for the close button. Use for internationalization.
collapseLabelcollapse-labelstring'Toggle sidebar'Accessible label for the collapse trigger button. Use for internationalization.
openopenbooleanfalseWhether the drawer is currently open
dockedAtdocked-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".
placementplacement'start' | 'end' | 'top' | 'bottom''end'Position of the drawer
sizesizeDrawerSize'medium'Size preset or custom CSS value
variantvariant'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
miniminibooleanfalseWhether the drawer is in mini/collapsed mode showing only icons
expandOnHoverexpand-on-hoverbooleanfalseWhether mini drawer should expand on hover
collapseTriggercollapse-triggerbooleanfalseWhether to show a collapse/expand trigger button in the header. Only for persistent/permanent variants.
noHeaderno-headerbooleanfalseWhether to hide the default header
modalmodalbooleantrueWhether the drawer behaves as a modal. Only meaningful for the temporary variant (persistent / permanent ignore it).
closeOnEscapeclose-on-escapebooleantrueWhether pressing Escape closes the drawer
closeOnOverlayClickclose-on-overlay-clickbooleantrueWhether clicking the overlay closes the drawer
returnFocusreturn-focusbooleantrueWhether to return focus to the trigger element on close
destroyOnClosedestroy-on-closebooleanfalseWhether to remove content from DOM when closed
containedcontainedbooleanfalseWhether 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.
forforstring''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.
focusSettledPromise<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

EventDetailDescription
showFired when the drawer opens. Detail: { source: HyVisibilitySource }
hideFired when the drawer closes (after animation). Detail: { source: HyVisibilitySource }
request-closeFired before closing; cancelable to prevent close. Detail: { source: HyVisibilitySource }
initial-focusFired on show after the autofocus discovery runs. Cancelable: call event.preventDefault() to skip the default focus and apply your own. Detail: HyInitialFocusDetail

Slots

SlotDescription
triggerAn 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.
defaultThe main content displayed in the drawer body
headerCustom header content that replaces the default title.
header-actionsAdditional actions in the header (alongside close button)
footerFooter content, typically action buttons
toggle-iconCustom collapse/expand icon inside the collapse trigger (overrides the default chevron)
close-iconCustom close icon inside the close button (overrides the default close icon)

CSS Parts

PartDescription
baseThe native <dialog> element
overlayThe backdrop overlay element
panelThe drawer panel container
headerThe header section containing title and close button
titleThe title element (h2) when using default header
header-actionsContainer for header action buttons
collapse-triggerThe collapse/expand trigger button in the header
close-buttonThe close button in the header
bodyThe scrollable content area
footerThe footer section for action buttons

CSS Custom Properties

PropertyDescription
--hy-drawer-overlay-backgroundBackdrop overlay background color (kept for HC fallbacks)
--hy-drawer-scrimBackdrop scrim color. Defaults to --hy-background-overlay-rest.
--hy-drawer-surfacePanel 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-foregroundPanel text color
--hy-drawer-strokeBorder width
--hy-drawer-borderBorder color
--hy-drawer-paddingThe panel inset (header top, footer bottom, every region's inline edges)
--hy-drawer-gap-regionsGap between header, body and footer
--hy-drawer-close-icon-sizeClose glyph size (default --hy-icon-size-md, 24px at standard density; the 48px target is kept)
--hy-drawer-padding-miniBody padding in mini mode
--hy-drawer-button-paddingClose/collapse button padding
--hy-drawer-gapGap between header/footer items
--hy-drawer-gap-actionsGap between header action buttons
--hy-drawer-button-radiusClose/collapse button border radius
--hy-drawer-mini-widthWidth of mini/collapsed drawer
--hy-drawer-size-smPanel width (or height for top/bottom) for size="small" (default: --hy-surface-width-sm)
--hy-drawer-size-mdPanel width (or height for top/bottom) for size="medium" (default: --hy-surface-width-md)
--hy-drawer-size-lgPanel width (or height for top/bottom) for size="large" (default: --hy-surface-width-lg)
--hy-drawer-button-hover-bgClose/collapse button hover background
--hy-drawer-body-shadowScroll shadow color for body overflow
--hy-drawer-background-overlay-strongForced-colors overlay background
--hy-drawer-shadowBox shadow for the drawer panel
--hy-drawer-font-familyFont family override for drawer title
--hy-drawer-enter-durationTransition duration for the open and close slide animation
--hy-drawer-enter-easingTransition easing for the open and close slide animation
--hy-drawer-expand-durationTransition duration for the expand and collapse animation
--hy-drawer-expand-easingTransition 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

Built with Lit. Documented with VitePress.