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… |
|---|---|
|
|
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.
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. Prefermode="onBlur"or the"onSubmit"default. - Do not call
handleSubmitmanually — let theForminvoke it after Zod validation passes. - Don't declare
fieldswith auseState/object literal recreated on every render withoutuseMemo— it causes the fields to re-mount and inputs to lose focus while typing. validationis Zod-only since v3 — Yup schemas are not supported.
See also
- Field — individual field wrapper with label, hint, and validation.
- Input, InputSelect, InputMask, InputCurrency, InputRange, TextArea, Checkbox, FieldGroup, UploadCard — all available fields.