HyDialog
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 variant="secondary">Cancel</hy-button>
<hy-button variant="primary">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="destructive">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). -->
<hy-dialog label="Edit Name">
<hy-text-input autofocus autoselect label="Name" value="John Doe"></hy-text-input>
<div slot="footer">
<hy-button variant="primary">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>Events
- 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
showbefore the autofocus discovery runs. Cancelable: callevent.preventDefault()to skip the default focus and apply your own. Detail:HyInitialFocusDetail
Slots
- trigger - An element that toggles the dialog on click (self-managing, no
openwiring required). ARIA (aria-haspopup,aria-controls,aria-expanded) is applied automatically and focus returns to the trigger on close. A controlledopenstill 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 thelabelprop) instead ofaria-labelledby. Ensure yourlabelprop accurately describes the dialog for screen readers. - footer - Footer content, typically action buttons
Methods
willUpdate()
Generates unique IDs before the first render to prevent update-after-update warnings. IDs are generated client-side only to prevent SSR/client hydration mismatches.
firstUpdated()
Resolve a for= trigger and apply initial trigger ARIA after the first render (the <dialog> id exists by now).
updated()
Single source of truth: all side effects for open/close happen here. The open property drives all dialog state changes.
disconnectedCallback()
Clean up when element is removed from the DOM. Restores body scroll if dialog was open when disconnected.
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)