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"

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 Features

  • 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

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)

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>

With Custom Header

html
<hy-dialog>
  <div slot="header">
    <hy-icon name="warning"></hy-icon>
    <span>Warning</span>
  </div>
  <p>This action cannot be undone.</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

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

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

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.
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.
describedBydescribed-bystring | undefinedID 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.
focusSettledPromise<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
showFired when the dialog opens. Detail: { source: HyVisibilitySource }
hideFired when the dialog closes (after animation). Detail: { source: HyVisibilitySource }
request-closeFired before closing; cancelable to prevent close. Detail: { source: HyVisibilitySource }
initial-focusFired 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
headerCustom header content that replaces the default title. Accessibility note: When using a custom header, the dialog uses aria-label (from the label prop) instead of aria-labelledby. Ensure your label prop accurately describes the dialog for screen readers.
footerFooter content, typically action buttons
close-iconCustom close icon inside the close button (overrides the default close icon)

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

PropertyDescription
--hy-dialog-background-overlay-restBackdrop overlay background color
--hy-dialog-background-overlay-strongForced-colors overlay background
--hy-dialog-overlay-shadowScroll shadow color for body overflow
--hy-dialog-background-surface-elevationDialog container 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-dialog-background-surface-subtleClose button hover background
--hy-dialog-border-default-restBorder color
--hy-dialog-foreground-default-restText and close button color
--hy-dialog-paddingThe panel inset (header top, footer bottom, every region's inline edges)
--hy-dialog-gap-regionsGap between header, body and footer
--hy-dialog-close-icon-sizeClose glyph size (default --hy-icon-size-md, 24px at standard density; the 48px target is kept)
--hy-dialog-close-paddingClose button padding
--hy-dialog-viewport-marginMargin from viewport edges (default: --hy-padding-layout-3xl)
--hy-dialog-gapGap between footer items
--hy-dialog-strokeBorder width
--hy-dialog-radiusContainer and close button border radius
--hy-dialog-widthWidth of the dialog container
--hy-dialog-scrimBackdrop scrim color. Defaults to --hy-background-overlay-rest.
--hy-dialog-shadowBox shadow for the dialog container
--hy-dialog-enter-durationTransition duration for the open and close animation
--hy-dialog-enter-easingTransition easing for the open and 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.