Trivela UI

Dialog

A modal built on the native dialog element, with variant and color token support.

Built on the native <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.

Rename project

This is how it appears everywhere in your workspace.

Usage

dialog-demo.tsx
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.

Rename project

This is how it appears everywhere in your workspace.

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.

Full-bleed banner

Ready to deploy

Build 1a2b3c passed every check.

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.

Terms

The panel scrolls; the page behind it does not.

Clause 1. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 2. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 3. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 4. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 5. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 6. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 7. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 8. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 9. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 10. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 11. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 12. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 13. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

Clause 14. The background is inert while this is open — try scrolling the page, or tabbing past the buttons below.

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

PropTypeDefaultDescription
openbooleanRequired. Controlled — the dialog never opens itself.
onClose() => voidRequired. Fires on Escape, the backdrop, or open going false.
titleReactNodeRenders an h2 and wires aria-labelledby. A dialog with no name announces nothing.
descriptionReactNodeRenders below the title and wires aria-describedby.
dismissOnBackdropbooleantrueWhether 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.
classNamestringMerged with the generated classes via cn().
DialogHeaderHTMLAttributes<HTMLDivElement>Title area. A flex column with gap-1.5 and p-6. Only needed for a header the title/description props cannot express.
DialogContentHTMLAttributes<HTMLDivElement>Body. p-6 pt-0 — add pt-6 yourself when there is no header above it.
DialogFooterHTMLAttributes<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.