Dialog
A modal surface that interrupts the flow to ask for input or confirmation.
Usage
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@kanzo-tech/ui";<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Claim contract</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader title="Something is eating the bell-ropes" description="Q-1041 · Thornmarch" />
<DialogBody>…</DialogBody>
</DialogContent>
</Dialog>DialogContent renders its own Portal, overlay and positioner, so a page never has to
assemble them. It is also lazyMount + unmountOnExit by default — the body is not in the
tree at all until the dialog opens, which keeps forms inside it from mounting on page load.
Anatomy
Dialog
├── DialogTrigger
└── DialogContent (portal + overlay + positioner)
├── DialogHeader
│ ├── DialogTitle
│ └── DialogDescription
├── DialogBody
├── DialogFooter
│ └── DialogClose
└── DialogClose (the built-in X)DialogHeader accepts title and description as props and renders the title/description
sub-components for you, so the common case is one line. Pass children instead when the header
needs more than text.
The three the tree names in a parenthesis are exported too. DialogContent renders them, so you
only reach for them to rearrange the floating layer: DialogPositioner, the box the content is
placed in, and DialogOverlay, the scrim behind it. Ark's Portal is the third.
Sizes
Controlled
onOpenChange receives a details object, not a boolean — read details.open.
open: false
Three surfaces, one machine
Dialog, Sheet and AlertDialog are the same Ark machine wearing
three shapes, which is why they share a header, a body and a title:
| What it is | Reach for it when | |
|---|---|---|
Dialog | a centred modal | the task is short and self-contained |
Sheet | the same modal docked to an edge | the panel wants the full height or width — a filter rail, a detail pane |
AlertDialog | the same modal as role="alertdialog", with no close button | dismissing by reflex would be the wrong outcome |
A Sheet has no SheetTitle and an AlertDialog has no separate title either: both render
DialogTitle, because one machine has one title.
Alert mode
AlertDialog is Dialog with role="alertdialog" and showCloseButton={false}. The X is removed on purpose, so the only
way out is an explicit Cancel or Confirm — reach for it when dismissing by reflex would be the
wrong outcome, and plain Dialog for everything else.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogClose,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTrigger,
} from "@kanzo-tech/ui";<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Abandon</Button>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader title="Abandon Q-1058?" description="The party is recalled and it is marked Failed." />
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogClose asChild>
<AlertDialogAction variant="destructive">Abandon</AlertDialogAction>
</AlertDialogClose>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>The parts mirror their Dialog counterparts one-to-one, with two additions in the footer:
AlertDialogFooter
├── AlertDialogCancel (outline Button + close trigger)
└── AlertDialogAction (default or destructive Button)AlertDialogBody is exported alongside them for prose longer than the header's description —
it is DialogBody with one rule added, dropping its top padding when an AlertDialogHeader is
already above it, so the two do not stack their spacing into a gap.
AlertDialogCancel already wraps itself in a close trigger. AlertDialogAction does not — it
is a plain Button, because the confirm path usually runs an async handler and decides for itself
when to close; wrap it in AlertDialogClose asChild when it should close immediately.
API Reference
dialogContentVariants and dialogOverlayVariants are exported for a floating layer you assemble
yourself — the size scale and the scrim, so a hand-built dialog stays the same box as this one.
Dialog
| Prop | Type | Default |
|---|---|---|
modal | boolean | true |
lazyMount | boolean | true |
unmountOnExit | boolean | true |
Extends React.ComponentProps<typeof ArkDialog.Root>. modal={false} also suppresses
DialogOverlay, so a non-modal dialog does not dim the page behind it.
DialogContent
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "fullscreen" | "md" |
bottomStickOnMobile | boolean | true |
showCloseButton | boolean | true |
bottomStickOnMobile turns the dialog into a bottom sheet below the sm breakpoint —
square top corners, full width, slide-up entrance — which is where a centred modal reads worst.
The close button is absolutely positioned, so DialogTitle adds inline-end padding whenever a
close trigger is present in the content. That is why a long title wraps early instead of
running under the X — and why setting showCloseButton={false} gives the padding back.
DialogHeader
| Prop | Type | Default |
|---|---|---|
title | string | — |
description | string | — |
DialogBody
| Prop | Type | Default |
|---|---|---|
scrollFade | boolean | false |
The body is wrapped in a ScrollArea, so long content scrolls inside the dialog while the
header and footer stay pinned. scrollFade adds the mask at the scroll edges.
AlertDialogAction
| Prop | Type | Default |
|---|---|---|
variant | "default" | "destructive" | "default" |
Narrowed from the full Button scale — a confirmation only ever affirms or destroys; every other
Button prop still passes through. The remaining AlertDialog* parts take the same props as their
Dialog counterparts, minus showCloseButton, which is forced off.