Skip to content

OverlaysDialog

Dialog

A modal dialog. Used for confirmations, the search palette, and any action that interrupts the user on purpose.

Native <dialog>Focus-trapEscape + backdrop close

Demo

The dialog uses the native <dialog> element. The showModal() API handles focus-trap and escape.

Default

Discard changes?

This will revert your edits to the last saved state.

Sizes

Sizes

sm

md

lg

Anatomy

  • Native <dialog> element, opened with showModal()
  • Backdrop: 40% ink at 40% opacity, blurs the page underneath
  • Surface: --surface-elevated with elevation-3 shadow
  • Padding: 24 px (1.5 rem)
  • Title: font-instrument-serif 1.25 rem / 1.2 line-height
  • Description: body, 0.9375 rem, --muted-foreground
  • Actions row: right-aligned, 8 px gap, primary on the right

Implementation

dialog.tsx
"use client"

import { useEffect, useRef, type ReactNode } from "react"
import { X } from "lucide-react"
import SiteIcon from "@/components/site-icon"

interface DialogProps {
  open: boolean
  onClose: () => void
  title: string
  description?: string
  size?: "sm" | "md" | "lg"
  primaryAction?: { label: string; onClick: () => void; tone?: "default" | "destructive" }
  secondaryAction?: { label: string; onClick: () => void }
  children?: ReactNode
}

export function Dialog({ open, onClose, title, description, size = "md", primaryAction, secondaryAction, children }: DialogProps) {
  const ref = useRef<HTMLDialogElement>(null)
  useEffect(() => {
    const el = ref.current
    if (!el) return
    if (open && !el.open) el.showModal()
    if (!open && el.open) el.close()
  }, [open])
  return (
    <dialog ref={ref} className={`ds-dialog ds-dialog--${size}`} onClose={onClose}>
      <div className="ds-dialog__head">
        <div>
          <p className="ds-dialog__title">{title}</p>
          {description ? <p className="ds-dialog__description">{description}</p> : null}
        </div>
        <button onClick={onClose} aria-label="Close" className="ds-dialog__close">
          <SiteIcon icon={X} size="sm" tone="muted" />
        </button>
      </div>
      {children ? <div className="ds-dialog__body">{children}</div> : null}
      {primaryAction || secondaryAction ? (
        <div className="ds-dialog__actions">
          {secondaryAction ? (
            <button className="ds-btn ds-btn--ghost ds-btn--sm" onClick={secondaryAction.onClick}>
              {secondaryAction.label}
            </button>
          ) : null}
          {primaryAction ? (
            <button
              className={`ds-btn ${primaryAction.tone === "destructive" ? "ds-btn--destructive" : "ds-btn--primary"} ds-btn--sm`}
              onClick={primaryAction.onClick}
            >
              {primaryAction.label}
            </button>
          ) : null}
        </div>
      ) : null}
    </dialog>
  )
}

Props

PropTypeDefaultDescription
openbooleanNoneRequired: controlled open state
onClose() => voidNoneRequired: close handler
titlestringNoneRequired: dialog title
descriptionstringNoneOptional dialog description
size"sm" | "md" | "lg""md"Max width

Accessibility

  • Native <dialog> with showModal() provides focus-trap and escape handling.
  • Title is the accessible name; description is announced as a description.
  • Body scroll is locked while the dialog is open.
  • Focus is moved to the primary action on open; restored to the trigger on close.
  • Backdrop click closes the dialog (when onClose is bound to the backdrop click).