Search for a command to run...
The one way to put a form in a dialog. Desktop is a Dialog; below 650px it is a DetachedSheet. Owns the form element, footer, sizing, pending state, and the scroll shell for tall forms.
Controlled usage with a mutation:
The component renders the <form> itself: native submission is prevented,
onSubmit fires on the submit button and on Cmd/Ctrl+Enter, and the footer is
wired without form="id" plumbing.
Desktop stays the Dialog card, including the built-in close button (hidden
while pending). Below the sheet breakpoint (650px) the same props render
as a DetachedSheet bottom sheet — pending still blocks Esc, backdrop, and
swipe; Cmd/Ctrl+Enter still submits; the error Alert still sits above the
fields. Both overlay roots stay mounted; only the open flag swaps, so a
resize never unmounts an open dialog.
The default is 512px wide and content-sized — it never scrolls.
variant="scroll" is for forms taller than the viewport: a fixed
h-[min(90svh,760px)] card at 672px with the header and footer floating over
the scroll area on a ProgressiveBlur. The height is definite on purpose —
ScrollArea silently won't scroll against a max-h auto-height parent.
FormDialog| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Names the action. |
description | ReactNode | — | What the form does or why. Required; pass descriptionSrOnly to hide it visually. |
descriptionSrOnly | boolean | false | Keeps the description for screen readers only. |
variant | "default" | "scroll" | "default" | default = 512px, content-sized. scroll = 672px, fixed height, floating header/footer. |
children | ReactNode | — | Fields, rendered inside a FieldGroup. |
error | ReactNode | — | Mutation-level error, rendered as one destructive Alert above the fields. |
submitLabel | string | — | Label for the submit button. |
cancelLabel | string | "Cancel" | Rarely overridden. |
destructive | boolean | false | The submit renders as the brand button; true swaps it to the destructive variant. |
pending | boolean | false | Disables both buttons, swaps a spinner into the submit, and blocks dismissal. |
submitDisabled | boolean | false | Disables only the submit — for forms gating on a required selection. |
onSubmit | () => void | — | Fires on submit; wire your form library's handleSubmit. |
trigger | ReactNode | — | Opens the dialog. Omit for controlled usage. |
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | Fires on open/close. Ignored for close attempts while pending. |
Reach for FormDialog for every form in an overlay. Dropping to raw Dialog or
DetachedSheet parts is for layouts this component genuinely can't express.
Give every field a FieldLabel, single-field dialogs included. Placeholders show format
examples; current values belong in defaultValue.
Use it for wizards, tabbed flows, or multi-step dialogs — those are a different pattern, not a form with extra steps.
Add a second submit or move actions into the body. A form dialog is a header, fields, and one Cancel/submit footer.