Skip to content

Modal

An accessible dialog built on the native <dialog> element with focus trap, Esc-to-close, scroll lock, and configurable sizes.

Import

import { Modal } from "@hyzer-labs/ui"

Demo

No dialog is open on page load. Click a trigger to open one.

The open and close animation honors --hz-duration-* and --hz-ease-*. See Motion for the token values, and for the script-side @hyzer-labs/ui/motion helpers built on them.

Example modal

Optional description line.

Modal body content. Focus is trapped while open.

Press Esc or click the backdrop to close.

<Button onclick={() => (open = true)}>Open modal</Button>

<Modal bind:open title="Example modal" description="Optional description line.">
	<p>Modal body content. Focus is trapped while open.</p>
	{#snippet actions()}
		<Button onclick={() => (open = false)}>Confirm</Button>
		<Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
	{/snippet}
</Modal>

Props

NameTypeDefaultNote
titlestringRequired for accessibility.
openbooleanfalse$bindable.
descriptionstring
size'sm' | 'md' | 'lg' | 'full''md'
closeOnOverlaybooleantrueEsc always closes regardless — the dialog pattern requires it.
showClosebooleantrue
preventScrollbooleantrue
closeLabelstring'Close dialog'
onclose() => void
classstringMerged after the hz-modal class.
childrenSnippet
actionsSnippet

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-modal

Data attributes

HookValuesStyles
data-size'sm' | 'md' | 'lg' | 'full'full goes edge-to-edge and drops the radius. Default md.
data-state'open' | 'closed'Mirrors the open prop. Visibility itself runs off the native [open] attribute that showModal() sets — this is the hook to animate against.
data-modal-closepresent on the close buttonLoad-bearing beyond styling: focus management queries it to keep the close button out of the first-focus candidates. Renaming it breaks focus order, not just looks.

Custom properties

HookValuesStyles
--hz-modal-width<length> — 30/40/52rem by sizeOne name across all three sizes: it overrides whichever size is active. data-size="full" ignores it.

Part classes

HookValuesStyles
.hz-modal-headerchild elementAlways rendered.
.hz-modal-titlechild elementAlways an h2 — Modal has no headingLevel prop, because the dialog is its own document section.
.hz-modal-descriptionchild elementThe optional description.
.hz-modal-bodychild elementAlways rendered, so the scroll region stays stable even when empty.
.hz-modal-footerchild elementPresent only with actions.
::backdroppseudo-elementThe native dialog backdrop.

Accessibility

Modal uses showModal() for native top-layer focus trapping and backdrop. aria-modal, aria-labelledby, and optional aria-describedby are set automatically. Focus returns to the trigger on close. The built-in close button's accessible name comes from closeLabel.

References: APG Dialog (Modal) pattern · MDN: <dialog>