Guides / Accessibility

Accessibility

Making your form accessible requires configuring each form element with proper attributes. But Conform can help you with that.

#Aria Attributes

When it comes to accessibility, aria attributes are usually the first thing that comes to mind, which usually requires some unique ids to associate different elements together. Conform helps you by generating all the ids for you.

1import { useForm } from '@conform-to/react';
2
3function Example() {
4  const [form, fields] = useForm();
5
6  return (
7    <form id={form.id}>
8      <label htmlFor={fields.message.id}>Message</label>
9      <input
10        type="text"
11        id={fields.message.id}
12        name={fields.message.name}
13        aria-invalid={!fields.message.valid ? true : undefined}
14        aria-describedby={
15          !fields.message.valid
16            ? `${fields.message.errorId} ${fields.message.descriptionId}`
17            : fields.message.descriptionId
18        }
19      />
20      <div id={fields.message.descriptionId}>The message you want to send</div>
21      <div id={fields.message.errorId}>{fields.message.errors}</div>
22      <button>Send</button>
23    </form>
24  );
25}

#Validation attributes

Validation attributes also play an important role in accessibility, such as improving the hint for screen readers. With Conform, you can derive the validation attributes from your zod or yup schema and have them populated on each field metadata.

1import { parseWithZod, getZodConstraint } from '@conform-to/zod';
2import { useForm } from '@conform-to/react';
3import { z } from 'zod';
4
5const schema = z.object({
6  message: z
7    .string()
8    .min(10)
9    .max(100)
10    .regex(/^[A-Za-z0-9 ]{10-100}$/),
11});
12
13function Example() {
14  const [form, fields] = useForm({
15    constraint: getZodConstraint(schema),
16    onValidate({ formData }) {
17      return parseWithZod(formData, { schema });
18    },
19  });
20
21  return (
22    <form id={form.id}>
23      <input
24        type="text"
25        name={fields.message.name}
26        required={fields.message.required}
27        minLength={fields.message.minLength}
28        maxLength={fields.message.maxLength}
29        pattern={fields.message.pattern}
30      />
31      <button>Send</button>
32    </form>
33  );
34}

#Progressive enhancement

Progressive enhancement also helps with accessibility, such as minimizing the impact of temporary network issues. For example, Conform make it possible to manipulate a list of fields with the form data and state persisted even across page refreshes.

1import { useForm } from '@conform-to/react';
2
3export default function Example() {
4  const [form, fields] = useForm();
5
6  return (
7    <form id={form.id}>
8      <ul>
9        {tasks.map((task) => (
10          <li key={task.key}>
11            <input name={task.name} defaultValue={task.initialValue} />
12            <button
13              {...form.remove.getButtonProps({
14                name: fields.tasks.name,
15                index,
16              })}
17            >
18              Delete
19            </button>
20          </li>
21        ))}
22      </ul>
23      <button
24        {...form.insert.getButtonProps({
25          name: fields.tasks.name,
26        })}
27      >
28        Add task
29      </button>
30      <button>Save</button>
31    </form>
32  );
33}

#Reducing boilerplate

Setting up all the attributes mentioned above can be tedious and error prone. Conform wants to help you with that by providing a set of helpers that derive all the related attributes for you.

Note: All the helpers mentioned are designed for the native HTML elements. You might not need them if you are using custom UI components, such as react-aria-components or Radix UI, which might already have the attributes set up for you through their own APIs.

Here is an example of how it compares to the manual setup. If you want to know more about the helpers, please check the corresponding documentation linked above

1import { parseWithZod, getZodConstraint } from '@conform-to/zod';
2import { useForm } from '@conform-to/react';
3import { z } from 'zod';
4
5const schema = z.object({
6  message: z
7    .string()
8    .min(10)
9    .max(100)
10    .regex(/^[A-Za-z0-9 ]{10-100}$/),
11});
12
13function Example() {
14  const [form, fields] = useForm({
15    constraint: getZodConstraint(schema),
16    onValidate({ formData }) {
17      return parseWithZod(formData, { schema });
18    },
19  });
20
21  return (
22    <form id={form.id}>
23      {/* Before */}
24      <input
25        type="text"
26        id={fields.message.id}
27        name={fields.message.name}
28        required={fields.message.required}
29        minLength={fields.message.minLength}
30        maxLength={fields.message.maxLength}
31        pattern={fields.message.pattern}
32        aria-invalid={!fields.message.valid ? true : undefined}
33        aria-describedby={
34          !fields.message.valid
35            ? `${fields.message.errorId} ${fields.message.descriptionId}`
36            : fields.message.descriptionId
37        }
38      />
39      {/* After */}
40      <input
41        {...getInputProps(fields.message, {
42          type: 'text',
43          ariaDescribedBy: fields.message.descriptionId,
44        })}
45      />
46      <button>Send</button>
47    </form>
48  );
49}