Docs
Form

Form

Declarative form: you describe the fields as data and the Form takes care of the rest — validation, responsive layout, error messages, normalized submit, and conditional fields.

Designed for those who need to ship complex forms without rewriting the infrastructure every time: sign-ups, onboarding, KYC, checkouts, and multi-step flows all render from the same configuration structure.

When to use

✅ Use when…🚫 Avoid when…
  • The form has more than one field and validation matters.
  • The fields can be declared as configuration (coming from a back-end, feature flag, or static).
  • You want to focus on the product domain, not on form wiring.
  • The layout needs to respond to breakpoints without extra CSS.
  • It is a single search/filter input — use the input directly.
  • You need a multi-step wizard with persistence across pages — combine several Forms or build the wizard manually.

Full demo

All of Apollion's form elements composed into a single Form — text, mask, select, async select, checkbox, grouped radio, range, currency, upload, textarea, and per-field validation.

Loading...

Basic form

<Form
  handleSubmit={(values) => console.log(values)}
  fields={{
    name: {
      label: 'Name',
      component: Input,
      validation: z.string().min(1, 'Required'),
      inputProps: { placeholder: 'Type your name' },
    },
    email: {
      label: 'Email',
      component: Input,
      validation: z.email('Required'),
      inputProps: { type: 'email', placeholder: 'you@company.com' },
    },
  }}
/>

Custom submit button

children accepts direct JSX or a (values) => ReactNode function when the button depends on the values:

<Form fields={fields} handleSubmit={onSubmit}>
  {({ acceptedTermsOfService }) => <Button disabled={!acceptedTermsOfService} text="Send It" />}
</Form>

Responsive layout

Each key in fields becomes a grid-area. Combine with medias for a layout that changes at breakpoints:

<Form
  handleSubmit={onSubmit}
  fields={fields}
  medias={{
    xs: { columns: '1fr', areas: '"name" "email" "button"' },
    sm: { columns: '2fr', areas: '"name email" "button button"' },
  }}
/>

Conditional fields

Render fields only when a condition over the current values is true.

<Form
  handleSubmit={onSubmit}
  conditionalFields={{
    spouse: (v) => v.maritalStatus === 'married',
  }}
  fields={{
    maritalStatus: { component: InputSelect, label: 'Marital status', inputProps: { options: ... } },
    spouse: { component: Input, label: 'Spouse name' },
  }}
/>

Typed validation

For TypeScript to infer inputProps based on component, declare the fields via newField:

const fields = {
  email: newField({
    component: Input,
    label: 'Email',
    inputProps: { type: 'email' }, // ← type-checked
    validation: z.email('Required'),
  }),
};

Gotchas

  • Avoid mode="onChange" on very large forms — it validates on every keystroke across every field. Prefer mode="onBlur" or the "onSubmit" default.
  • Do not call handleSubmit manually — let the Form invoke it after Zod validation passes.
  • Don't declare fields with a useState/object literal recreated on every render without useMemo — it causes the fields to re-mount and inputs to lose focus while typing.
  • validation is Zod-only since v3 — Yup schemas are not supported.

See also