Dialog

Dialog is a controlled modal with portal rendering, focus trapping, scroll locking, and accessible escape routes built in.

Examples

Confirming a destructive action

The footer is where actions belong — the cancel path first, the destructive one last.

Blocking accidental dismissal

Turn off closeOnEsc and closeOnOverlayClick only when losing the dialog would lose work or skip a decision. Never remove every close path: the × renders whenever onClose is set.

When to use

Use a Dialog for a short, focused decision that must be resolved before the user moves on: confirming a deletion, naming a new resource, reviewing terms.

Reach for something else when:

  • The content is long or the task is a side journey — use a Drawer, which slides in without claiming the whole screen.
  • You only need to report the outcome of something — use a Toast (transient) or an Alert (persistent, inline).
  • The panel is anchored to a control and holds choices — use a Dropdown.

Accessibility

  • The panel is role="dialog" with aria-modal, named by title via aria-labelledby.
  • Focus moves into the panel on open, is trapped while open, and returns to the element that opened it on close.
  • Every close path calls onClose: the × button, Escape, and a backdrop click. The × renders only when onClose is provided, so a dialog is never left without a visible way out.
  • The × button is named "Close" by default. Pass closeLabel to translate its accessible name.
  • Page scroll is locked while the dialog is open, and the panel is portaled to document.body so it escapes any overflow: hidden ancestor.

Props

NameTypeRequiredDescription
openbooleanRequiredControls visibility. `true` mounts the overlay + panel; `false` plays the exit then unmounts.
onClose() => voidCalled by every close path (Esc, overlay click, the × button). The × renders only when this is provided.
closeLabelstringAccessible name for the close button. Default: `"Close"`.
titleReactNodeRequiredHeading rendered in the panel header and wired as the accessible name via `aria-labelledby`.
footerReactNodeAction buttons rendered in the footer. Omitted → no `.lyra-dialog__footer` chrome.
closeOnEscbooleanClose on the Escape key. Default `true`.
closeOnOverlayClickbooleanClose on a click on the overlay backdrop (never on panel content). Default `true`.
containerHTMLElementPortal host. Defaults to `document.body`.
childrenReactNodeRequiredBody content.

Plain HTML

Without React, compose the same classes — you own the open/close state, focus and Escape:

html
<div class="lyra-dialog-overlay">
  <div class="lyra-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title">
    <div class="lyra-dialog__header">
      <h2 class="lyra-dialog__title" id="dialog-title">Delete project</h2>
      <button class="lyra-dialog__close" aria-label="Close"></button>
    </div>
    <div class="lyra-dialog__body">This action cannot be undone.</div>
    <div class="lyra-dialog__footer">
      <button class="lyra-btn lyra-btn--ghost lyra-btn--md">Cancel</button>
      <button class="lyra-btn lyra-btn--danger lyra-btn--md">Delete</button>
    </div>
  </div>
</div>