Dialog
A modal dialog — Attio's create-record surface (focus-trapped, scroll-locked).
The modal behind Attio's create-record flow: a radius-16 card with a layered float shadow, a bordered header (title + close), a scrollable body, and a bordered footer for the primary action. It composes Zag's dialog machine, so focus trapping, scroll lock, and Escape / outside-click dismissal come from the machine rather than hand-rolled handlers. Give it a title and compose the content with DialogBody / DialogFooter.
Preview
Installation
bun add crispimport "crisp/styles.css";
import { Dialog, DialogBody, DialogFooter } from "crisp";Usage
import { Dialog, DialogBody, DialogFooter } from "crisp";
const [open, setOpen] = useState(false);
<Dialog open={open} onOpenChange={setOpen} title="New company" width={520}>
<DialogBody>
<div className="dialog-field">
<label className="dialog-field-label" htmlFor="name">Name</label>
<input id="name" placeholder="Set Name…" />
</div>
</DialogBody>
<DialogFooter>
<button className="dialog-submit" onClick={save}>
Create record <span className="dialog-submit-key">⌘↵</span>
</button>
</DialogFooter>
</Dialog>;API
Dialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state — the caller owns when the dialog shows. |
onOpenChange | (open: boolean) => void | — | Fires on Escape, outside-click, and the close button. |
title | ReactNode | — | Header title; when set, the header (title + close) renders. |
icon | ReactNode | — | Leading icon shown before the title. |
width | number | 640 | Content width in px, clamped to 90vw. |
aria-label | string | — | Accessible name when title is not a plain string. |
Also exported: DialogBody (scrollable, gap-stacked content region) and DialogFooter (right-aligned action bar). The reusable form classes — dialog-field, dialog-field-label, dialog-more, dialog-submit, dialog-submit-key — style stacked label/input rows and the primary action.
Notes
- Focus & scroll. Opening traps focus inside the dialog and locks background scroll; closing returns focus to the trigger — all from the Zag machine.
- Dismissal. Escape, an outside pointer-down, and the header close button all call
onOpenChange(false); the caller decides whether to actually close. - Portal. The dialog renders in a portal at the document root, so it is never clipped by an ancestor's
overflow— style its content through thedialog-*classes rather than a wrapping selector.