Forms
TaylorUI uses react-aria-components to build accessible forms and provides a set of pre-built form elements.
- Support for client-side and server side validation
- All form components meet our A11y standards
Getting started
Step 1. Make sure these 3rd party dependencies are installed with at least the following versions:
| 3rd Party Libraries | version | Install command |
|---|---|---|
react-aria-components | ^1.18.0 | npm install react-aria-components@^1.18.0 |
tw-animate-css | ^1.4.0 | npm install tw-animate-css@^1.4.0 |
Step 2. Copy the forms folder from @/ui/components.
1. The useForm hook
A form management hook that handles submission state, validation errors, and server errors.
| Prop | Type | Description |
|---|---|---|
id | string | Optional form ID. Default: generated with useId() |
onSubmit | function | The description of the text field. |
const form = useForm<ValuesType>({
onSubmit: ({ values }) => {
await signupToMailchimp(values);
}
});2. The <FormRoot> component
This component provides the form context to all form elements. It should wrap the heading, <FormStatus> and the <form> itself.
The <FormRoot> component wraps your form in a <FormContext.Provider> and renders a <div> with the attribute data-form-root="true"
and a dynamic data-status attribute that reflects the current submission status (idle, pending, success, error).
This enables styling based on form state using CSS attribute selectors, e.g. [data-form-root][data-status="error"].
The syntax for the <FormRoot> component is as follows:
<FormRoot context={form}> // the context is what's returned from the useForm hook
<h2>Sign up to our newsletter</h2>
<FormStatus>
{form.serverError && (
<FormServerError title="Something went wrong!" errorMessage={form.serverError} />
)}
<FieldErrorSummary
title="Something went wrong!"
subtitle="Please correct your input in these fields:"
fieldLabels={...}
/>
</FormStatus>
<Form>
...
</Form>
</FormRoot>| Prop | Type | Description |
|---|---|---|
context | FormController | Required. The form controller object returned from the useForm hook. Provides form state, validation errors, submission handlers, and server error to all descendant form elements via context. |
className | string | Optional additional CSS class names applied to the root <div> wrapper. |
props | ComponentProps<'div'> | All other standard HTML div attributes are forwarded to the wrapper element. |
3. The <FormStatus> component
The <FormStatus> component is a wrapper that groups success and error messages for A11Y.
When submitting an erreneous form, the <FormStatus> component will automatically focus the first error message and announce it to screen readers.
Inside <FormStatus> you can use the <FormServerError> and <FieldErrorSummary> components to display server-side and client-side validation errors.
Read more about the
3.1 The <FormServerError> component
This component displays a server-side error message.
<FormServerError errorMessage={form.serverError} title="Could not register" />| Prop | Type | Description |
|---|---|---|
errorMessage | ReactNode | The error message to display. |
title | ReactNode | Title of the error alert box. |
3.2 The <FieldErrorSummary> component
This component displays a summary of all invalid fields in the form. It suppresses itself when there are no validation errors.
For each invalid field, it displays the human-readable label of the field. You can provide a mapping of field names to their labels via the fieldLabels prop.
<FieldErrorSummary
title="There are errors in your submission"
subtitle="Please correct your input in these fields:"
fieldLabels={{
name: 'Name',
email: 'Email Address',
phone: 'Phone Number',
...
}}
/>| Prop | Type | Description |
|---|---|---|
title | ReactNode | The title of the error alert. |
subtitle | ReactNode | The subtitle of the error alert. |
 fieldLabels | Record<string, string> | A mapping of field names to their human-readable labels. Used to generate the list of invalid fields in the summary. |
4. The <Form> component
Use the <Form> component inside <FormRoot> to render the actual <form> element. It wires up the onSubmit and onInvalid events from the form context automatically.
| Prop | Type | Description |
|---|---|---|
onSubmit | React.FormEventHandler<HTMLFormElement> | Optional callback fired on form submission. |
onInvalid | React.FormEventHandler<HTMLFormElement> | Optional callback fired when the form is invalid. |
props | Omit<ComponentProps<typeof AriaForm>, 'validationErrors'> | All other standard <form> attributes are forwarded |
Create an account
'use client';
import {
RiEarthLine,
RiHeart2Line,
RiLockPasswordLine,
RiPhoneLine,
} from '@remixicon/react';
import Link from 'next/link';
import { Button } from '@/ui/components/button/button';
import { Checkbox } from '@/ui/components/forms/checkbox';
import {
Combobox,
ComboboxItem,
ComboboxListBox,
ComboboxSearchField,
} from '@/ui/components/forms/combobox';
import { Form, FormRoot } from '@/ui/components/forms/form';
import { useForm } from '@/ui/components/forms/hooks/use-form';
import { Radio, RadioGroup } from '@/ui/components/forms/radio';
import { Select, SelectItem } from '@/ui/components/forms/select';
import { TextField } from '@/ui/components/forms/text-field';
import {
FieldErrorSummary,
FormStatus,
ServerError,
validators,
} from '@/ui/components/forms/validation';
import { registerAction } from './action';
import { RegistrationFormValues } from './types';
export const RegistrationForm = () => {
const form = useForm<RegistrationFormValues>({
onSubmit: async ({ values, reset }) => {
await registerAction(values);
reset();
},
});
return (
<FormRoot context={form}>
<h2 id="registration-form-heading" className="mb-4 text-3xl font-bold">
Create an account
</h2>
<FormStatus>
<FormServerError
errorMessage={form.serverError}
title="Could not register"
/>
<FieldErrorSummary
title="Something went wrong!"
subtitle="The form could not be submitted because some of the fields are invalid. Please correct your input in the following fields:"
fieldLabels={{
name: 'Name',
email: 'Email Address',
phone: 'Phone Number',
password: 'Password',
favoriteAnimal: 'Favorite Animal',
contactMethod: 'Preferred Contact Method',
country: 'Country',
'terms-and-conditions': 'Terms and Conditions',
}}
/>
</FormStatus>
<Form
className="flex flex-col gap-4 rounded-lg border-2 border-gray-200 p-6 shadow-xl"
aria-labelledby="registration-form-heading"
>
<TextField
id="name"
name="name"
label="Name"
type="text"
description="Please enter your full name without any middle names"
autoComplete="name"
isRequired
validate={(value) => {
if (!value) {
return 'Please enter your name';
}
}}
/>
<TextField
id="email"
name="email"
label="Email Address"
type="email"
description="Please enter a valid email address. Example: test@test.com"
autoComplete="email"
validate={(value) => {
if (!validators.email(value)) {
return 'Invalid email address';
}
}}
isRequired
/>
<TextField
id="phone"
name="phone"
label="Phone Number"
type="tel"
icon={<RiPhoneLine size={20} />}
description="Please enter your phone number with the country code. Example: +43"
autoComplete="tel"
/>
<TextField
id="password"
name="password"
label="Password"
type="password"
description="Password must be at least 8 characters long and contain at least one
uppercase letter, one lowercase letter, and one number."
autoComplete="new-password"
icon={<RiLockPasswordLine size={18} />}
isRequired
validate={(value) => {
if (value.length < 8) {
return 'Password must be at least 8 characters long';
}
if (!/[A-Z]/.test(value)) {
return 'Password must contain at least one uppercase letter';
}
if (!/[a-z]/.test(value)) {
return 'Password must contain at least one lowercase letter';
}
if (!/[0-9]/.test(value)) {
return 'Password must contain at least one number';
}
}}
/>
<Select
id="favoriteAnimal"
name="favoriteAnimal"
label="Favorite Animal"
icon={<RiHeart2Line size={18} />}
isRequired
validate={(value) => {
if (!value) {
return 'Please select an animal';
}
}}
>
<SelectItem id="cat">Cat</SelectItem>
<SelectItem id="dog">Dog</SelectItem>
<SelectItem id="kangaroo">Kangaroo</SelectItem>
<SelectItem id="panda">Panda</SelectItem>
<SelectItem id="snake">Snake</SelectItem>
<SelectItem id="lion">Lion</SelectItem>
<SelectItem id="tiger">Tiger</SelectItem>
<SelectItem id="elephant">Elephant</SelectItem>
<SelectItem id="giraffe">Giraffe</SelectItem>
<SelectItem id="zebra">Zebra</SelectItem>
<SelectItem id="hippo">Hippo</SelectItem>
<SelectItem id="rhino">Rhino</SelectItem>
<SelectItem id="leopard">Leopard</SelectItem>
<SelectItem id="cheetah">Cheetah</SelectItem>
<SelectItem id="monkey">Monkey</SelectItem>
<SelectItem id="fox">Fox</SelectItem>
<SelectItem id="wolf">Wolf</SelectItem>
</Select>
<Combobox
id="country"
name="country"
label="Country"
isRequired
icon={<RiEarthLine size={18} />}
validate={(value) => {
if (!value) {
return 'Please select a country';
}
}}
>
<ComboboxSearchField placeholder="Search country..." />
<ComboboxListBox>
<ComboboxItem id="austria">Austria</ComboboxItem>
<ComboboxItem id="ireland">Ireland</ComboboxItem>
<ComboboxItem id="poland">Poland</ComboboxItem>
<ComboboxItem id="uk">United Kingdom</ComboboxItem>
<ComboboxItem id="hungary">Hungary</ComboboxItem>
<ComboboxItem id="spain">Spain</ComboboxItem>
<ComboboxItem id="portugal">Portugal</ComboboxItem>
<ComboboxItem id="kangaroo">Australia</ComboboxItem>
<ComboboxItem id="in">India</ComboboxItem>
<ComboboxItem id="br">Brazil</ComboboxItem>
<ComboboxItem id="mx">Mexico</ComboboxItem>
<ComboboxItem id="it">Italy</ComboboxItem>
<ComboboxItem id="es">Spain</ComboboxItem>
<ComboboxItem id="se">Sweden</ComboboxItem>
<ComboboxItem id="no">Norway</ComboboxItem>
<ComboboxItem id="fi">Finland</ComboboxItem>
</ComboboxListBox>
</Combobox>
<RadioGroup
id="contactMethod"
name="contactMethod"
label="Preferred Contact Method"
description="We will contact you via the method you select"
isRequired
validate={(value) => {
if (!value) {
return 'Please select a contact method';
}
if (value === 'pigeon') {
return 'Please select a modern contact method';
}
}}
orientation="vertical"
>
<Radio value="email">Email</Radio>
<Radio value="phone">Phone</Radio>
<Radio value="pigeon">Carrier Pigeon</Radio>
</RadioGroup>
<Checkbox
id="terms-and-conditions"
name="terms-and-conditions"
value="terms-and-conditions"
label="I agree to the terms and conditions"
description={
<>
Read our full{' '}
<Link
href="/terms-and-conditions"
className="text-primary-500 hover:underline"
>
Terms and Conditions
</Link>
</>
}
isRequired
validate={(value) => {
if (!value) {
return 'Please agree to the terms and conditions';
}
}}
className="pt-2"
/>
<div>
<Button
type="submit"
disabled={form.isPending}
loading={form.isPending}
>
Create your account
</Button>
</div>
</Form>
</FormRoot>
);
};