Design system/Components
Dialog
The dialog is the app's highest interactive layer: it opens centered on desktop and rises from the bottom like a sheet on phones, with a handle you can drag down.
In codecomponents/knowledge/ui.tsxcomponents/confirm.tsxcomponents/sheet-grow.tsx
Live example
Open the dialog. Tap the backdrop, press Escape, or tap ✕; all three close it.
Anatomy
- BackgroundA 45 percent warm dim with a 3px blur; tapping it closes the dialog.
- HeaderAn icon in a soft orange circle, a 15px heading, a subheading, and a close button.
- BodyThe only part that scrolls.
- FooterButtons, separated by a dashed line; always visible.
On phones
Below 640px, every .modal becomes full width, sticks to the bottom of the screen, gets 24px top corners, and rises with the sheet curve. The handle is drawn with ::before, with no extra markup. SheetGrow lets you drag the sheet to full screen or down.
This behavior lives in CSS for every dialog, not in each component; a new dialog is correct on phones with no extra work.
460pxDialog: default width; .modal itself 440px78vhPhone 88vh18pxPhone 24px 24px 0 0z 1200popIn 0.3s springPhone 0.34s sheetWhen
When to use it
- A short task you need to finish before moving on: creating, editing a record, confirming.
- A form that depends on the current page and shouldn't leave it.
When not to
- Long reading or a multi-step task; build it its own page.
- Reporting something that just happened; a toast is enough.
- A dialog on top of a dialog.
Code
<Dialog
open={open}
onClose={() => setOpen(false)}
title={tr("اقدام تازه")}
icon={paths.plus}
footer={
<>
<button className="btn btn-ghost flex-1" onClick={() => setOpen(false)}>{tr("انصراف")}</button>
<button className="btn btn-dark flex-1" onClick={save}>{tr("ساخت اقدام")}</button>
</>
}
>
<TextField floating label={tr("عنوان")} value={title} onChange={(e) => setTitle(e.target.value)} />
</Dialog>Sizes
padding 20px 20px 12px · gap 10px36px · icon 16px--accent on --accent-soft15px / 600 · truncate11.5px · --text-secondary30px · radius 999px · icon 15pxHover: --red on --red-softpadding 0 20px 16px · gap 14pxpadding 12px 20px 20px · gap 8px · dashed-topmin-width 128px · end of the rowflex 1 1 0 · min-height 48px · radius 12pxrgba(20, 16, 12, 0.45) · blur(3px)100dvh · radius 0data-expanded with SheetGrowAccessibility
- Escape closes it through a keydown listener on window; a higher Floating panel catches Escape first, so one Escape doesn't close two layers.
- The close button has the aria-label and title “Close”.
- Dialog and DialogShell have role="dialog", aria-modal="true", aria-labelledby pointing to the title, and tabIndex=-1; ConfirmDialog has the same with role="alertdialog".
- When it opens, focus moves to the element with data-autofocus; if there isn't one, to the first focusable element; and if there isn't one either, to the dialog itself. Tab and Shift+Tab cycle inside as long as this dialog is the topmost .modal; a Floating panel opened from inside it keeps its own focus. On close, focus returns to whatever opened the dialog. All of this is useDialogA11y in lib/use-dialog-a11y.ts.
- Closing happens on mousedown on the backdrop; mousedown inside the dialog is stopped, so dragging text from inside to outside doesn't close the dialog.
- With reduced motion, the phone dialog fades in over 0.18 seconds instead of sliding up.
Do
A title that says what's being created or changed: “New action”.
Don't
“Form” or “Edit” with no object.
Do
The primary action goes last in the footer; on desktop the row is aligned to the end.
Don't
Buttons that stretch edge to edge on desktop; the footer no longer allows it.
Do
A long body scrolls while the header and footer stay fixed.
Don't
Scrolling the whole dialog, which pushes the footer buttons out of view.
Related pages