Dialog
The design-system dialog panel, with a coloured top rail, a title, a body and Cancel and Confirm buttons.
import { Dialog } from "@/ui";Dialog matches the Figma Dialog_info component. It is the panel that the library, publishing, workflow review and workbook screens use for confirmations and short forms. It draws the frame: a 4px coloured rail along the top, a state icon, the title and a close button, your content, and a footer with Cancel on the left and Confirm on the right.
Dialog is only the panel. It has no open prop, no backdrop, no role="dialog", and it does not handle Escape or focus. You render it when it should show and wrap it yourself, as every screen that uses it does. See Opening a dialog. For how it compares with Modal and which to use, see Modal.
Anatomy
Shown here without its backdrop so you can see the parts.
<Dialog size="sm" title="Rename dashboard" confirmLabel="Rename" onClose={close} onCancel={close} onConfirm={save}>
<TextInput label="Dashboard name" value={name} onChange={(e) => setName(e.target.value)} />
</Dialog>| Part | Style |
|---|---|
| Panel | White, --radius-lg (12px) corners, a soft 0 8px 12px shadow at 10% grey. |
| Rail | 4px top border in --color-primary-500, or --color-orange (#FBBF24) and --color-red for the warning and error states. |
| Header | 16px by 12px padding. A 24px icon in the rail colour, 6px gap, then the title in --font-size-h2 (16px) semibold, --color-text-primary, on one line. A 16px close icon on the right, in a 24px hit area. |
| Body | 16px by 12px padding. Children are stacked with a 24px gap (--spacing-24). |
| Footer | A 1px --color-grey-100 line above, 16px by 12px padding. Cancel is a ghost Button, Confirm a primary Button. |
Opening a dialog
Keep an open flag in state and render the Dialog inside a backdrop only while it is true. The wrapper gives it the dialog role and a name, closes on Escape and on a click outside, moves focus into the dialog when it opens and back to the trigger when it closes.
function PublishDialog({ changes, onPublish, onClose }) {
const box = useRef(null);
useEffect(() => {
const back = document.activeElement;
box.current?.querySelector("input, textarea, button:not([aria-label='Close'])")?.focus();
const onKey = (e) => e.key === "Escape" && onClose();
window.addEventListener("keydown", onKey);
return () => { window.removeEventListener("keydown", onKey); back?.focus?.(); };
}, [onClose]);
return (
<div className="fixed inset-0 z-[80] grid place-items-center p-4 bg-[rgba(31,33,46,0.45)]" onClick={onClose}>
<div ref={box} role="dialog" aria-modal="true" aria-label="Publish 3 changes?" onClick={(e) => e.stopPropagation()}>
<Dialog
size="md"
title="Publish 3 changes?"
confirmLabel="Publish"
onClose={onClose}
onCancel={onClose}
onConfirm={() => { onPublish(); onClose(); }}
>
<ChangeList changes={changes} />
<p>They go to the Recommendations page for everyone who can see this workflow. Nothing changes in your ad accounts until someone applies a recommendation.</p>
</Dialog>
</div>
</div>
);
}
{open && <PublishDialog changes={changes} onPublish={publish} onClose={() => setOpen(false)} />}Wire all three handlers. onClose runs from the close icon and onCancel from the Cancel button. Neither closes the dialog by itself.
Sizes
Pick the size by what is inside, not by how important the dialog is. The examples below open with the wrapper above.
<Dialog size="sm" title="Rename workbook" confirmLabel="Rename">…</Dialog>
<Dialog size="md" title="Save as a template" confirmLabel="Save template">…</Dialog>
<Dialog size="lg" title="Import from your website" confirmLabel="Read my website">…</Dialog>| Size | Width | Footer buttons | Use for |
|---|---|---|---|
sm | 460px | md | One field or one sentence. The body centres its content and is at least 70px tall. |
md | 650px | lg | A short form or a list of what will change. Content is left-aligned. |
lg | 800px | lg | Previews and multi-step flows. Content is left-aligned. |
The widths are fixed. On a narrow screen, cap them through className, as the workflow review does with max-width: calc(100vw - 32px).
States
state sets the rail and icon colour, and the icon: Info, Warning or XCircle. In the error state Confirm turns into a red Button, so a destructive action reads as one.
Open them over the page:
<Dialog size="sm" state="warning" title="Unsaved changes" confirmLabel="Leave">…</Dialog>
<Dialog size="sm" state="error" title="Delete page?" confirmLabel="Delete">…</Dialog>| State | Rail and icon | Icon | Confirm |
|---|---|---|---|
default | --color-primary-500 | Info | primary |
warning | --color-orange | Warning | primary |
error | --color-red | XCircle | red |
Use confirmDisabled to grey out Confirm until the form is valid. This is the disabled button Figma draws in its warning and error examples. Use confirmVariant to pick another Button variant.
Figma uses Warning and XCircle icons only on Small dialogs. Its Medium and Large dialogs show the Info icon in every state, just recoloured. Code uses the state's icon at every size. Pass icon={null} for no icon, as the Dependency dialog and Widget access dialog do.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
size | "sm" | "md" | "lg" | "sm" | 460, 650 or 800px wide. |
state | "default" | "warning" | "error" | "default" | Rail, icon and, for error, a red Confirm. |
title | string | "" | Shown on one line and not truncated, so keep it short. |
icon | Phosphor icon | null | by state | null hides it. |
children | ReactNode | The body. | |
cancelLabel | string | "Cancel" | |
confirmLabel | string | "Confirm" | Name the action: "Publish", "Rename", "Save template". |
onClose | () => void | The close icon. | |
onCancel | () => void | The Cancel button. | |
onConfirm | () => void | The Confirm button. | |
confirmVariant | Button variant | red for error, else primary | |
confirmDisabled | boolean | false | |
footer | ReactNode | false | Replaces Cancel and Confirm, or false hides the footer. | |
className | string | On the panel. Use it for width limits or to adjust .dialog__body from your own CSS. |
Other props (aria-*, id, data-*) are passed to the panel. ref is not forwarded.
Accessibility
What Dialog gives you:
- The close icon is a
<button>witharia-label="Close". The footer actions are product Buttons.
What the wrapper has to add, because Dialog does not:
role="dialog",aria-modal="true"and a name. The title is a plain<span>, so pass the same text asaria-labelon the wrapper.- Escape to close. The library and workflow screens add their own key listener. The workbook rename dialog has none, so Escape does nothing there.
- Focus. Move focus into the dialog when it opens (the first field, or the first button) and back to the trigger when it closes, as the example does.
- Focus does not stay inside. Tab moves past the last button and on into the page behind the backdrop. None of the current wrappers trap it.
Do and don't
- Do use Dialog for new confirmations and short forms, inside the wrapper above.
- Do write the title as a question or an action ("Publish 3 changes?", "Rename dashboard") and the Confirm label as the verb.
- Do say in the body what will happen, including what will not change.
- Don't put a long form or a whole workflow in a dialog. Give it a page.
- Don't open a dialog from inside another dialog.
- Do use the error state for a destructive action, so Confirm is red.
- Don't use the warning state just to draw attention. Use it when going ahead loses something, like unsaved edits.