Dialog
A modal built on the native dialog element, with variant and color token support.
<dialog>. The focus trap, Escape, the inert background, returning focus to whatever opened it, and the top layer all come from the browser — showModal()does the work. The top layer is also why the panel escapes any ancestor's overflow: hidden or z-index without needing a portal.It is controlled:
open is yours, and onClose fires however the dialog closed — including Escape, which the browser handles without asking. Requires registry/styles/dialog.css for the backdrop, since ::backdrop is a pseudo-element no utility can reach.The panel itself has no padding — DialogHeader, DialogContent and DialogFooter carry it, exactly as Card does, so the two cannot double up and a header can run edge to edge. The
title/description props render their own DialogHeader, so the common path needs no wrapper.Usage
import { Dialog, DialogContent, DialogFooter, Button } from 'trivela-ui';
export function Example() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Rename project</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
title="Rename project"
description="This is how it appears everywhere."
>
<DialogContent>
<Input defaultValue="trivela-ui" />
</DialogContent>
<DialogFooter>
<Button variant="ghost" color="neutral" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={() => setOpen(false)}>Save</Button>
</DialogFooter>
</Dialog>
</>
);
}Examples
Basic
Open it, then press Escape, click the backdrop, or Tab around — focus stays inside and returns to the button when it closes. None of that is code in this repo.
Composed header
Reach for DialogHeader only when the title/description props cannot express the header — an icon, a close button, a full-bleed image. Passing both would render two headers, and the aria wiring becomes yours, because only the props know which node is the name.
Long content
The panel scrolls at max-h and the page behind it is locked — showModal makes the background inert but does not stop it scrolling, so the component does that part.
Backdrop dismissal
On by default. Turn it off when a stray click should not count as an answer — which is exactly what AlertDialog does.
A backdrop click reports the <dialog>itself as its target — but so does a click on the panel's own padding. The component measures the pointer against the panel's box to tell the two apart, so padding clicks do not close it.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Required. Controlled — the dialog never opens itself. |
| onClose | () => void | — | Required. Fires on Escape, the backdrop, or open going false. |
| title | ReactNode | — | Renders an h2 and wires aria-labelledby. A dialog with no name announces nothing. |
| description | ReactNode | — | Renders below the title and wires aria-describedby. |
| dismissOnBackdrop | boolean | true | Whether a click outside the panel closes it. |
| role | 'dialog' | 'alertdialog' | 'dialog' | AlertDialog sets this to alertdialog. |
| variant | 'solid' | 'ghost' | 'outline' | 'subtle' | 'solid' | Style variant for the component. |
| color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'neutral' | Color scale used by the component. |
| size | 'sm' | 'md' | 'lg' | 'md' | Max width, and the padding of its sections. |
| className | string | — | Merged with the generated classes via cn(). |
| DialogHeader | HTMLAttributes<HTMLDivElement> | — | Title area. A flex column with gap-1.5 and p-6. Only needed for a header the title/description props cannot express. |
| DialogContent | HTMLAttributes<HTMLDivElement> | — | Body. p-6 pt-0 — add pt-6 yourself when there is no header above it. |
| DialogFooter | HTMLAttributes<HTMLDivElement> | — | Action row. p-6 pt-0, stacked reversed on a phone so the primary action sits under the thumb while DOM order stays correct for keyboard. |