Single Page Form
When to Use
- A routed Create or Edit page that the design has explicitly called out
- Moderate field count (roughly 6–15) without natural sectioning, completed in one pass
Page Implementation
tsx
function FormSinglePage() {
const onSave = (values: ProductDraft) => window.alert(`Saving ${values.name}`);
const onCancel = () => {};
return (
<Layout>
{/*
* Save/Cancel live in the page header and reach the form through the
* native `form` attribute, matching `Form`'s `id`. No state plumbing.
*/}
<Layout.Header
title="Create product"
actions={[
<Button key="cancel" variant="outline" onClick={onCancel}>
Cancel
</Button>,
<Button key="save" type="submit" form="product-form">
Save
</Button>,
]}
/>
<Layout.Column>
<Form<ProductDraft>
id="product-form"
noValidate
className="max-w-2xl space-y-4"
onFormSubmit={(values) => onSave(values)}
>
<Field.Root name="name">
<Field.Label>Name</Field.Label>
<Field.Control required />
<Field.Error match="valueMissing">Name is required.</Field.Error>
</Field.Root>
<Field.Root name="sku">
<Field.Label>SKU</Field.Label>
<Field.Control required pattern="[A-Z]{3}-[0-9]{4}" />
<Field.Description>Format: ABC-1234</Field.Description>
<Field.Error match="patternMismatch">Use the format ABC-1234.</Field.Error>
<Field.Error match="valueMissing">SKU is required.</Field.Error>
</Field.Root>
{/*
* Dropdowns need no `name` and no React state: `Field.Root` registers
* the control, so its value arrives in `onFormSubmit` under the
* field's name like any other input.
*/}
<Field.Root name="category">
<Field.Label>Category</Field.Label>
<Select items={CATEGORIES} placeholder="Select category" />
<Field.Error />
</Field.Root>
<Field.Root name="price">
<Field.Label>Price</Field.Label>
<Field.Control type="number" required min={0} step="0.01" />
<Field.Error match="rangeUnderflow">Price cannot be negative.</Field.Error>
<Field.Error match="valueMissing">Price is required.</Field.Error>
</Field.Root>
<Field.Root name="description">
<Field.Label>Description</Field.Label>
<Textarea rows={4} />
</Field.Root>
</Form>
</Layout.Column>
</Layout>
);
}Constraints
- Single column full width below 1024px; single column max-w constrained at 1024–1280px
- Without an explicit routed-page requirement, the answer is
form/modal - A
/createor/editroute in the screen spec does NOT require a full-page replacement - Save/Cancel belong in
Layout.Header, wired with<Button type="submit" form="…">matching theForm'sid(requires 1.13.0+) - Validation lives on the fields (
required,pattern,min,type) with a matchingField.Error match="…";Formgates submit on it
Form state
Rules that apply to every form/* pattern are in components.md → Forms, with a worked example in form/modal → Form state: Form + Field is the default stack, submit via onFormSubmit, dropdowns need only a wrapping Field.Root (no name, no state), server errors route through errors, and React Hook Form is an optional escape hatch.
This pattern is the reference for the simple case: every field, dropdowns included, is uncontrolled and arrives through onFormSubmit. No component state at all.
Anti-patterns
- Two-column layout for unrelated fields — breaks the linear reading order
- No required-field markers — users can't predict which fields will error
- Errors shown above the form rather than below the offending field
- Choosing this pattern for a Create flow because it's a Create flow — without explicit need, use
form/modal - Making fields controlled "for consistency" — nothing on this page needs React state
- Giving
Selectanameto make it submit — inside aField.Rootthat is already handled