Skip to content
View as Markdown

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 /create or /edit route in the screen spec does NOT require a full-page replacement
  • Save/Cancel belong in Layout.Header, wired with <Button type="submit" form="…"> matching the Form's id (requires 1.13.0+)
  • Validation lives on the fields (required, pattern, min, type) with a matching Field.Error match="…"; Form gates 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 Select a name to make it submit — inside a Field.Root that is already handled