Form
A composition-first form for building typed, validated forms from reusable pieces. The field components are pre-bound to Apollo’s Field primitives, so a form is assembled declaratively instead of wiring inputs by hand.
What it is built on
The Form component is a thin Apollo layer over two libraries, so you get their behavior without the boilerplate:
- TanStack Form handles state, subscriptions, and submission.
useAppFormis created once viacreateFormHookand wired to the Apollo field components, so each field is rendered withform.AppField. - Zod describes the shape and rules. Zod v4 implements the Standard Schema spec, so a schema passes directly into TanStack Form validators. No resolver or adapter is required.
Installation
npx shadcn@latest add @uipath/formHow it works
form.AppFieldprovides the field context. Its render prop exposes the bound components (field.TextField,field.SelectField, etc).form.AppFormprovides the form context to form-level components such asSubmitButtonandFormErrorSummary.form.SubmitButtonsubscribes tocanSubmitand disables itself while the form is invalid or submitting.
Usage
Validation is defined once as a whole-form Zod schema passed to validators.onChange.
import * as z from 'zod'
import { FieldGroup } from '@/components/ui/field'
import { useAppForm } from '@/components/ui/form'
const schema = z.object({
fullName: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.email('Enter a valid email address.'),
acceptTerms: z.boolean().refine((value) => value, {
message: 'You must accept the terms.',
}),
})
function SignUpForm() {
const form = useAppForm({
defaultValues: { fullName: '', email: '', acceptTerms: false },
validators: { onChange: schema },
onSubmit: ({ value }) => console.log(value),
})
return (
<form
onSubmit={(event) => {
event.preventDefault()
void form.handleSubmit()
}}
>
<form.AppForm>
<FieldGroup>
<form.AppField name="fullName">
{(field) => <field.TextField label="Full name" />}
</form.AppField>
<form.AppField name="email">
{(field) => <field.TextField type="email" label="Email" />}
</form.AppField>
<form.AppField name="acceptTerms">
{(field) => <field.CheckboxField label="I accept the terms" />}
</form.AppField>
<form.SubmitButton>Create account</form.SubmitButton>
</FieldGroup>
</form.AppForm>
</form>
)
}Field components
| Component | Value type | Notes |
|---|---|---|
TextField | string | Wraps Input, forwards native input props. |
TextareaField | string | Wraps Textarea. |
SelectField | string | Takes an options array of { label, value }. |
RadioGroupField | string | Takes an options array of { label, value }. |
CheckboxField | boolean | Horizontal layout with label and description. |
SwitchField | boolean | Horizontal layout with label and description. |
Translatable validation errors
Error messages are resolved through react-i18next. Each message is passed to t(message, { defaultValue: message }), so a message that matches a translation key is localized, and a plain sentence still renders as-is. Use a translation key as the Zod message to make an error translatable.
const schema = z.object({
email: z.email('form_error_email_invalid'),
})Two components consume these messages:
- Per field: the bound field components render their own translated errors automatically. For a custom field, use
FieldErrorwith the field’s raw errors. - Form level:
FormErrorSummarylists every field’s errors in one translated, accessible block. Render it insideform.AppForm.
<form.AppForm>
<form.FormErrorSummary title="Please fix the following:" />
{/* fields */}
</form.AppForm>Field-level validation
The whole-form schema is the recommended default, but individual fields can add their own validators. Field-level validators run alongside the form schema.
<form.AppField
name="email"
validators={{ onChange: z.email('Enter a valid email address.') }}
>
{(field) => <field.TextField type="email" label="Email" />}
</form.AppField>