OverlaysDialog
Dialog
A modal dialog. Used for confirmations, the search palette, and any action that interrupts the user on purpose.
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 withshowModal() - Backdrop: 40% ink at 40% opacity, blurs the page underneath
- Surface:
--surface-elevatedwithelevation-3shadow - Padding: 24 px (1.5 rem)
- Title:
font-instrument-serif1.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
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | None | Required: controlled open state |
onClose | () => void | None | Required: close handler |
title | string | None | Required: dialog title |
description | string | None | Optional dialog description |
size | "sm" | "md" | "lg" | "md" | Max width |
Accessibility
- Native
<dialog>withshowModal()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
onCloseis bound to the backdrop click).