hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

Dialog#

An accessible modal dialog built on the native <dialog> element. The browser supplies the focus trap, background inert, top layer, and Escape-to-close; hono-preact-ui adds the ARIA wiring, open-state, and render-prop composition. It ships unstyled: style it through the data-state contract.

Demo#

Subscribe

Get notified when we ship something new.

Styling#

Every part exposes data-state="open" | "closed". The popup is a native <dialog>, so its backdrop is the ::backdrop pseudo-element and its entry animates with @starting-style (degrading to no animation where unsupported). The demo above uses the styles below; copy a starting point in either flavor:

/* Center the modal and give it a card surface. position + transform restore
   centering even when a reset (e.g. Tailwind preflight) clears the UA margin
   that normally centers a modal <dialog>. Keep centering in `transform`, not the
   standalone `translate` property: a rule that sets both can have its `translate`
   dropped by a CSS minifier, silently un-centering the dialog in a production
   build. */
dialog[data-state='open'] {
  position: fixed;
  top: 50%;
  left: 50%;
  width: min(28rem, calc(100vw - 2rem));
  max-height: calc(100dvh - 2rem);
  overflow: auto;
  padding: 1.5rem;
  border: 1px solid #e4e4e7;
  border-radius: 14px;
  background: #fff;
  color: #18181b;
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.2),
    0 4px 6px -4px rgb(0 0 0 / 0.2);
  opacity: 1;
  transform: translate(-50%, -50%);
  transition:
    opacity 160ms ease,
    transform 160ms ease;
}

@starting-style {
  dialog[data-state='open'] {
    opacity: 0;
    transform: translate(-50%, calc(-50% + 8px)) scale(0.98);
  }
}

dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
  backdrop-filter: blur(2px);
  opacity: 1;
  transition: opacity 160ms ease;
}

@starting-style {
  dialog[open]::backdrop {
    opacity: 0;
  }
}

@media (prefers-color-scheme: dark) {
  dialog[data-state='open'] {
    border-color: #27272a;
    background: #18181b;
    color: #fafafa;
  }
}

dialog[data-state='closed'] {
  animation: dialog-out 160ms ease-in forwards;
}
@keyframes dialog-out {
  to {
    opacity: 0;
    transform: translate(-50%, calc(-50% + 8px)) scale(0.98);
  }
}

dialog[data-state='closed']::backdrop {
  animation: dialog-backdrop-out 160ms ease-in forwards;
}
@keyframes dialog-backdrop-out {
  to {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  dialog[data-state='open'],
  dialog[data-state='closed'],
  dialog::backdrop,
  dialog[data-state='closed']::backdrop {
    transition: none;
    animation: none;
  }
}

API reference#

Every part accepts a render prop for composition (see renderElement) and forwards unknown props to the element it renders. Dialog.Trigger, Dialog.Popup, and Dialog.Close also expose data-state="open" | "closed" and pass { open } to a render function.

Dialog.Root#

Provides state and id wiring to the parts. Renders only its children.

PropTypeDefaultDescription
openboolean-Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the dialog requests an open or close.
childrenComponentChildren-The trigger and popup.

Dialog.Trigger#

Opens the dialog on click. Default element <button type="button">.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Trigger label.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded to the element; a passed onClick runs before the dialog opens.

Sets aria-haspopup="dialog", aria-expanded, aria-controls, id, and data-state.

Dialog.Popup#

The native <dialog>. Renders closed on the server and calls showModal() on the client when opened.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
aria-labelstring-Accessible name when there is no Dialog.Title.
closeOnBackdropClickbooleantrueClose when the backdrop (the dialog's own area) is clicked.
childrenComponentChildren-Title, description, body, and close controls.
...propsJSX.HTMLAttributes<HTMLDialogElement>-Forwarded to the element.

Sets id, data-state, aria-labelledby (the Title) or aria-label, and aria-describedby (only when a Dialog.Description is present). role="dialog" and aria-modal="true" come implicitly from showModal().

Dialog.Title#

The dialog's accessible name, wired to the popup's aria-labelledby. Default element <h2>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-Title text.
...propsJSX.HTMLAttributes<HTMLHeadingElement>-Forwarded to the element.

Dialog.Description#

Optional supporting text, wired to the popup's aria-describedby while it is rendered. Default element <p>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
childrenComponentChildren-Description text.
...propsJSX.HTMLAttributes<HTMLParagraphElement>-Forwarded to the element.

Dialog.Close#

Closes the dialog on click. Default element <button type="button">.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Button label.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded to the element; a passed onClick runs before the dialog closes.

Primitives#

hono-preact-ui also exports the building blocks the parts use, for composing your own components. Each has its own page with examples: renderElement, useControllableState, and mergeRefs.

Accessibility#

A dialog must have an accessible name: render a Dialog.Title (wired through aria-labelledby) or pass aria-label to Dialog.Popup. A Dialog.Description is wired through aria-describedby when present.

Because the popup is a native modal <dialog>, the platform handles the rest:

  • Focus moves into the dialog when it opens and is trapped until it closes.
  • Background content is inert: not focusable and hidden from the accessibility tree.
  • Escape closes the dialog and focus returns to the trigger.
  • The dialog renders in the top layer, above all other content, with the ::backdrop covering the page.
  • Screen readers announce the dialog's name, and its description when present.