HyDialog
Usage examples on this page are written for Lit / plain HTML (
<hy-dialog>). The same component ships asHyDialogin@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
::backdroppseudo-element for overlay styling - Escape Key Handling: Built-in keyboard dismissal with the
cancelevent - Accessibility: Implicit
role="dialog"andaria-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-transitioningattribute - Closing: Reverse animation with
transitionendevent 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
<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)
<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
<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)
<hy-dialog no-header label="Image Preview">
<img src="preview.jpg" alt="Preview image" />
</hy-dialog>Programmatic Control
<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
<!-- 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
<!-- 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)
<!-- 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
<!-- 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)
<!-- 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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
titleId | title-id | string | '' | — |
label | label | string | 'Dialog' | — |
closeLabel | close-label | string | 'Close' | Accessible label for the close button. Use for internationalization. |
open | open | boolean | false | — |
noHeader | no-header | boolean | false | — |
persistent | persistent | boolean | false | When true, the dialog cannot be dismissed by backdrop-click or Escape. The explicit close button and programmatic .hide() / open = false still close. |
describedBy | described-by | string | undefined | — | ID of an element that describes the dialog content. |
for | for | string | '' | 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
| Event | Detail | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
trigger | An 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. |
default | The main content displayed in the dialog body |
header | Custom 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. |
footer | Footer content, typically action buttons |
close-icon | Custom close icon inside the close button (overrides the default close icon) |
CSS Parts
| Part | Description |
|---|---|
base | The native <dialog> element |
container | The dialog container with background, border, and shadow |
header | The header section containing title and close button |
title | The title element (h2) when using default 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-dialog-background-overlay-rest | Backdrop overlay background color |
--hy-dialog-background-overlay-strong | Forced-colors overlay background |
--hy-dialog-overlay-shadow | Scroll shadow color for body overflow |
--hy-dialog-background-surface-elevation | Dialog 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-subtle | Close button hover background |
--hy-dialog-border-default-rest | Border color |
--hy-dialog-foreground-default-rest | Text and close button color |
--hy-dialog-padding | The panel inset (header top, footer bottom, every region's inline edges) |
--hy-dialog-gap-regions | Gap between header, body and footer |
--hy-dialog-close-icon-size | Close glyph size (default --hy-icon-size-md, 24px at standard density; the 48px target is kept) |
--hy-dialog-close-padding | Close button padding |
--hy-dialog-viewport-margin | Margin from viewport edges (default: --hy-padding-layout-3xl) |
--hy-dialog-gap | Gap between footer items |
--hy-dialog-stroke | Border width |
--hy-dialog-radius | Container and close button border radius |
--hy-dialog-width | Width of the dialog container |
--hy-dialog-scrim | Backdrop scrim color. Defaults to --hy-background-overlay-rest. |
--hy-dialog-shadow | Box shadow for the dialog container |
--hy-dialog-enter-duration | Transition duration for the open and close animation |
--hy-dialog-enter-easing | Transition 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)