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 LibrariesversionInstall command
react-aria-components^1.18.0npm install react-aria-components@^1.18.0
tw-animate-css^1.4.0npm install tw-animate-css@^1.4.0

Step 2. Copy the forms folder from @/components.

1. The useForm hook

A form management hook that handles submission state, validation errors, and server errors.

PropTypeDescription
idstringOptional form ID. Default: generated with useId()
onSubmitfunctionThe 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>
PropTypeDescription
contextFormControllerRequired. 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.
classNamestringOptional additional CSS class names applied to the root <div> wrapper.
propsComponentProps<'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" />
PropTypeDescription
errorMessageReactNodeThe error message to display.
titleReactNodeTitle 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',
    ...
  }}
/>
PropTypeDescription
titleReactNodeThe title of the error alert.
subtitleReactNodeThe subtitle of the error alert.
 fieldLabelsRecord<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.

PropTypeDescription
onSubmitReact.FormEventHandler<HTMLFormElement>Optional callback fired on form submission.
onInvalidReact.FormEventHandler<HTMLFormElement>Optional callback fired when the form is invalid.
propsOmit<ComponentProps<typeof AriaForm>, 'validationErrors'>All other standard <form> attributes are forwarded

Create an account

Please enter your full name without any middle names
Please enter a valid email address. Example: test@test.com
Please enter your phone number with the country code. Example: +43
Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number.
Favorite Animal (Required)
Country (Required)
Preferred Contact Method (Required)We will contact you via the method you select
Read our full Terms and Conditions
'use client';
 
import {
  RiEarthLine,
  RiHeart2Line,
  RiLockPasswordLine,
  RiPhoneLine,
} from '@remixicon/react';
import Link from 'next/link';
import { Button } from '@/components/button/button';
import { Checkbox } from '@/components/forms/checkbox';
import {
  Combobox,
  ComboboxItem,
  ComboboxListBox,
  ComboboxSearchField,
} from '@/components/forms/combobox';
import { Form, FormRoot } from '@/components/forms/form';
import { useForm } from '@/components/forms/hooks/use-form';
import { Radio, RadioGroup } from '@/components/forms/radio';
import { Select, SelectItem } from '@/components/forms/select';
import { TextField } from '@/components/forms/text-field';
import {
  FieldErrorSummary,
  FormStatus,
  ServerError,
  validators,
} from '@/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>
  );
};