Skip to content

HyDialog ​

Usage examples on this page are written for Lit / plain HTML (<hy-dialog>). The same component ships as HyDialog 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 modal dialog component using the native HTML <dialog> element with showModal().

Native Dialog Approach ​

This implementation leverages the browser's native <dialog> element which provides:

  • Top Layer Rendering: Dialog appears above all other content automatically
  • Inert Background: Content outside the dialog is 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" (role="alertdialog" with alert)

Focus Trap (JS-supplemented) ​

Native <dialog>.showModal() makes background content inert, but Tab cycling within the dialog is browser-dependent — Firefox in particular lets focus drift into browser chrome before returning. On show, the dialog activates a JS focus trap (internal/focus-trap.ts) that intercepts Tab/Shift+Tab and cycles focus across the close button + all slotted focusables. On hide / disconnect, the trap is torn down so the trigger element can receive focus restoration.

Animation System ​

The dialog uses CSS transitions for smooth open/close animations:

  • Opening: Scale and opacity transition triggered via data-transitioning attribute
  • Closing: Reverse animation with transitionend event detection
  • Reduced Motion: Instant transitions for users who prefer reduced motion

Accessibility: built in ​

  • Native dialog accessibility (role="dialog", aria-modal="true")
  • Configurable aria-labelledby and aria-label
  • Focus restoration to trigger element on close
  • Escape key dismissal with cancelable event
  • Backdrop click dismissal
  • Body scroll lock when open

Accessibility: your part ​

  • Give every dialog a label that names its task. Keep it accurate under no-header, since the label is then the name alone.
  • Never put autofocus on a destructive action. In an alert dialog put it on the least destructive one, usually Cancel.

Browser Compatibility ​

Native <dialog> with showModal() is supported in:

  • Chrome 37+ (August 2014)
  • Firefox 98+ (March 2022)
  • Safari 15.4+ (March 2022)
  • Edge 79+ (January 2020)

When to use

  • A decision people must make before going on: confirm a deletion, discard changes.
  • A short task that interrupts the page on purpose: rename, a quick form of a few fields.

Do

  • Make the title the question or the task: "Delete this project?"
  • Name the confirming action by what it does: "Delete project", not "Yes".
  • Use alert for a confirmation of a destructive action.

When not to use

  • Information that needs no answer, or the result of an action that just ran: hy-toast, or hy-alert in the page.
  • A long or multi-step task, or one that needs the page beside it: its own page, or hy-drawer.
  • Details or a small form that belong next to the control that opened them: hy-popover.
  • Updating your own open state: do it on hide. request-close also fires for closes the dialog refuses, such as a backdrop click on an alert dialog.

Examples ​

Basic Usage ​

html
<hy-dialog label="Confirm Action" open>
  <p>Are you sure you want to proceed?</p>
  <div slot="footer">
    <hy-button emphasis="plain">Cancel</hy-button>
    <hy-button variant="brand">Confirm</hy-button>
  </div>
</hy-dialog>

Self-managing trigger (no open wiring) ​

html
<hy-dialog label="Confirm Action">
  <hy-button slot="trigger">Open dialog</hy-button>
  <p>Are you sure you want to proceed?</p>
</hy-dialog>

Without Header (Headless) ​

html
<hy-dialog no-header label="Image Preview">
  <img src="preview.jpg" alt="Preview image" />
</hy-dialog>

Programmatic Control ​

html
<hy-button id="openBtn">Open Dialog</hy-button>
<hy-dialog id="myDialog" label="Settings">
  <p>Configure your preferences here.</p>
</hy-dialog>

<script>
  const dialog = document.getElementById('myDialog');
  const openBtn = document.getElementById('openBtn');

  openBtn.addEventListener('click', () => dialog.show());

  // Listen for close requests (escapable)
  dialog.addEventListener('request-close', (e) => {
    if (hasUnsavedChanges) {
      e.preventDefault(); // Keep dialog open
      showUnsavedWarning();
    }
  });

  // Listen for visibility changes
  dialog.addEventListener('show', () => console.log('Opened'));
  dialog.addEventListener('hide', () => console.log('Closed'));
</script>

With Described Content ​

html
<!-- Note: described-by works best when the referenced element is outside the dialog -->
<p id="delete-description" hidden>
  Deleting this item will permanently remove it from your account.
</p>
<hy-dialog label="Delete Item" described-by="delete-description">
  <p>Deleting this item will permanently remove it from your account.</p>
  <div slot="footer">
    <hy-button variant="danger">Delete</hy-button>
  </div>
</hy-dialog>

Autofocus a slotted form control ​

html
<!-- The dialog discovers the [autofocus] descendant on `show` and focuses it.
For text-like controls, add `autoselect` to pre-select the value so the
user can type-to-replace (rename-style flow). The public `focusSettled`
promise resolves once this sequence has settled. -->
<hy-dialog label="Edit Name">
  <hy-text-input autofocus autoselect label="Name" value="John Doe"></hy-text-input>
  <div slot="footer">
    <hy-button variant="brand">Save</hy-button>
  </div>
</hy-dialog>

Autofocus by marker (preferred) ​

html
<!-- Mark the slotted control with `autofocus`; the dialog discovers and
focuses it on `show`. Pair with `autoselect` on text-like inputs to
pre-select the value (rename-style flow). -->
<hy-dialog label="Rename">
  <hy-text-input label="Name" value="Old name" autofocus autoselect></hy-text-input>
</hy-dialog>

Custom initial focus via cancelable event ​

html
<!-- For the rare case where the wanted target lives outside the dialog
or focus must depend on runtime state, listen for `initial-focus`
and call `event.preventDefault()` to opt out of the default. -->
<hy-dialog id="edit-dialog" label="Edit">
  <input id="alt-target" />
</hy-dialog>
<script>
  document.getElementById('edit-dialog').addEventListener('initial-focus', (e) => {
    e.preventDefault();
    document.getElementById('alt-target').focus();
  });
</script>

Confirmation (alert dialog) ​

html
<!-- role="alertdialog": the body is read as the description on open, and a
backdrop click does not dismiss. Escape and the close button still
close (both are Cancel). Focus the least destructive action. -->
<hy-dialog alert label="Delete project?">
  <p>This permanently deletes the project and its versions. This can't be undone.</p>
  <div slot="footer">
    <hy-button emphasis="plain" autofocus>Cancel</hy-button>
    <hy-button variant="danger">Delete</hy-button>
  </div>
</hy-dialog>

Persistent Dialog (must use explicit close) ​

html
<!-- Backdrop-click and Escape are ignored; only the close button works -->
<hy-dialog label="Saving…" persistent>
  <p>Uploading your file. Please wait for this to finish.</p>
</hy-dialog>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
titleIdtitle-idstring''—
labellabelstring'Dialog'—
closeLabelclose-labelstring'Close'Accessible label for the close button. Use for internationalization.
closedLabelclosed-labelstring'{label} dialog closed'The announcement read when the dialog closes; {label} is the dialog's label. Use for internationalization.
openopenbooleanfalse—
noHeaderno-headerbooleanfalse—
persistentpersistentbooleanfalseWhen true, the dialog cannot be dismissed by backdrop-click or Escape. The explicit close button and programmatic .hide() / open = false still close.
sizesize'small' | 'medium' | 'large''large'Width of the dialog: small, medium or large, each the design system's surface width of that size (--hy-surface-width-sm / -md / -lg, tuned per dialog through --hy-dialog-surface-width-*), or any CSS length as an escape hatch. The dialog never grows past the window, whatever the size. large is the width every dialog had before the option existed. The content never sets the width: content wider than the dialog's column makes the body scroll sideways, and a development warning names it — let the text wrap.
alertalertbooleanfalseAlert dialog, for destructive or irreversible confirmations ("Delete this? This can't be undone."). The panel takes role="alertdialog", so assistive tech reads the message as soon as it opens: the body is the dialog's description unless described-by names one. A backdrop click does not dismiss it (request-close still fires with source: 'backdrop-click'); Escape and the close button still close, since both are Cancel — add persistent to block Escape too (a confirm whose action is in flight).
describedBydescribed-bystring | undefined—ID of an element that describes the dialog content.
forforstring''Id of an external trigger element that toggles the dialog. Fallback for the slotted trigger pattern when the trigger must live outside the dialog (e.g. SSR). Resolved by walking the shadow-host chain. The slotted trigger slot takes precedence when both are present.
focusSettled—Promise<void>—Resolves once the current open's initial-focus sequence has settled — discovery ran and focus was applied (or fell through to the native showModal default), or the sequence was skipped (prevented initial-focus, closed before the deferred work ran). Before the first open it is already resolved.

Events ​

EventDetailDescription
show—Fired when the dialog opens. Detail: { source: HyVisibilitySource }
hide—Fired when the dialog closes (after animation). Detail: { source: HyVisibilitySource }
request-close—Fired before closing; cancelable to prevent close. Detail: { source: HyVisibilitySource }
initial-focus—Fired on show before 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 dialog 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.
defaultThe main content displayed in the dialog body
footerFooter content, typically action buttons

CSS Parts ​

PartDescription
baseThe native <dialog> element
containerThe dialog container with background, border, and shadow
headerThe header section containing title and close button
titleThe title element (h2) when using default header
close-buttonThe close button in the header
bodyThe scrollable content area
footerThe 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.

PropertyDefaultDescription
--hy-dialog-background-overlay-strongvar(--hy-background-overlay-strong)Forced-colors overlay background
--hy-dialog-overlay-shadow-colorvar(--hy-overlay-shadow-color)Scroll shadow color, shown while the body scrolls down
--hy-dialog-background-surface-elevation-5var(--hy-background-surface-elevation-5)Dialog container background color. (same as surface-base in light mode; lifts in dark mode for OLED-true-black visibility)
--hy-dialog-close-background-muted-hovervar(--hy-background-muted-hover)Close button hover background
--hy-dialog-radius-action-mdvar(--hy-radius-action-md)Close button border radius
--hy-dialog-border-elevated-restvar(--hy-border-elevated-rest)Border color of the panel (its rim)
--hy-dialog-border-default-restvar(--hy-border-default-rest)Border color of the header and footer hairlines while the body scrolls
--hy-dialog-foreground-default-restvar(--hy-foreground-default-rest)Text and close button color
--hy-dialog-padding-overlay-modal-mdvar(--hy-padding-overlay-modal-md)The panel inset (header top, footer bottom, every region's inline edges)
--hy-dialog-padding-overlay-modal-smvar(--hy-padding-overlay-modal-sm)The body's inset beside a scroll hairline (header / footer side)
--hy-dialog-gap-overlay-modal-lgvar(--hy-gap-overlay-modal-lg)Gap between header, body and footer
--hy-dialog-icon-size-mdvar(--hy-icon-size-md)Close glyph size (24px at standard density; the 48px target is kept)
--hy-dialog-padding-layout-xsvar(--hy-padding-layout-xs)Close button padding
--hy-dialog-padding-layout-3xlvar(--hy-padding-layout-3xl)Margin from viewport edges
--hy-dialog-gap-overlay-modal-mdvar(--hy-gap-overlay-modal-md)Gap between footer items
--hy-dialog-stroke-smvar(--hy-stroke-sm)Border width
--hy-dialog-radius-overlay-modal-mdvar(--hy-radius-overlay-modal-md)Container and close button border radius
--hy-dialog-surface-width-smvar(--hy-surface-width-sm)Width of the dialog container for size="small"
--hy-dialog-surface-width-mdvar(--hy-surface-width-md)Width of the dialog container for size="medium"
--hy-dialog-surface-width-lgvar(--hy-surface-width-lg)Width of the dialog container for size="large" (the default)
--hy-dialog-scrim-background-overlay-restvar(--hy-background-overlay-rest)Backdrop scrim color.
--hy-dialog-shadow-elevation-5var(--hy-shadow-elevation-5)Box shadow for the dialog container
--hy-dialog-motion-transition-enter-durationvar(--hy-motion-transition-enter-duration)Transition duration for the open animation
--hy-dialog-motion-transition-enter-easingvar(--hy-motion-transition-enter-easing)Transition easing for the open animation
--hy-dialog-motion-transition-exit-durationvar(--hy-motion-transition-exit-duration)Transition duration for the close animation
--hy-dialog-motion-transition-exit-easingvar(--hy-motion-transition-exit-easing)Transition easing for the close animation

Methods ​

show() ​

Opens the dialog programmatically. Sets the open property to true; all side effects happen in updated().

hide() ​

Closes the dialog programmatically. Emits a cancelable request-close event first. If the event is prevented, the dialog remains open.

Parameters:

  • source - How the close was triggered (for event detail)

Built with Lit. Documented with VitePress.