Form dialog
A form behind a button: fields, steps and errors that behave the same in every dialog.
import { FormDialog, FormField, FormStep and 1 more } from "@arcadia/ui"The home of every form that is not open on a page. The page shows a row with its current value and one button; the button opens this. Good design removes what it can while keeping what it does: three password fields at rest become one button, and the fields appear when someone asks to change it.
onSubmit resolves to close the dialog (the caller toasts in three words or fewer) or rejects. A FormError naming a field puts its message under that field, invalid and focused with its text selected; any other rejection is one line above the footer. A FormField's rule shows only after the field is left once or a submit is tried, never while the first characters are typed.
Examples
Focus goes to the first field on open, and back to the button that opened it on close. Tab runs the fields, then Cancel or Back, then the primary. An error is announced, and the field it names is marked invalid. While a submit is in flight, Escape and a press outside wait for it. Under 480px the footer's buttons take a line each, the primary first; under reduced motion a step changes with a fade.
- Enter
- Continues, or submits on the last step
- Esc
- Closes and discards what was typed
- Tab
- Fields, then Cancel or Back, then the primary
FormDialog
openrequired- boolean
onOpenChangerequired- (open: boolean) => void
titlerequired- ReactNode
description- ReactNode
- The consequence of the action, when there is one. A FormStep's own description replaces it on that step.
size- "md" | "lg"Default "md"
lg480px.submitLabelrequired- string
- The primary's label on the last step ("Change password").
tone- "neutral" | "danger"Default "neutral"
onSubmitrequired- () => Promise<void> | void
- Resolves on success (the dialog closes; the caller toasts in three words or fewer). Rejects with a FormError to show its message, on a field and step when it names them; any other rejection is the form's message.
canSubmit- booleanDefault true
- The primary is enabled when true.
name- string
- The form's name, rendered as
data-formon the dialog: what a test or a product rule finds it by. container- PortalContainer
- Where a surface's portal mounts. Default:
document.body. Pass a themed subtree to keep itsdata-theme, or a transformed stage to containposition: fixedoverlays (previews). childrenrequired- ReactNode
- Fields (FormField + Input), or FormStep children for a form in steps.
namerequired- string
- The
nameof the input inside: a FormError names it, and its rules wait for it to be left once. rule- string | null
- The message while the value breaks a rule ("At least 12 characters"), else null: shown after the field's first blur or the first submit, never while the first characters are typed.
label- ReactNode
hint- string
- Why or how, for a field its label cannot explain: a
Hintafter the label, and the control's description. At most 160 characters. description- ReactNode
- Help text under the label (text-3, 12/16).
invalid- boolean
disabled- boolean
- Disables the control and dims the label.
rootStyle- RootStyle
- Typed StyleX override: where the field sits.
className- string
- A product's hook class for its own layout rules. Being retired: new code places a field with
rootStyle; the call sites left are counted by ui.lint (kit-classname) and only go down.
Rest props go to <div> props.
nextLabel- string
- The primary's label on this step (default "Continue"; the last step's is the dialog's
submitLabel). canContinue- booleanDefault true
- The primary is enabled when true.
onContinue- () => void | Promise<void>
- A check before the next step: throw a FormError to stay with a message. Nothing is sent by default.
description- ReactNode
- The dialog's description while this step shows (the consequence of the last step belongs on it).