# Validate a form

> Show an error next to a field in a way a screen reader hears, and put focus on the field that needs fixing.

Source: https://platform.azmara.io/docs/recipes/form-validation

Validate when the form is submitted, show each message next to its field, and move focus to the first field with a problem.

```tsx
"use client";

import { Button } from "@azmr/ui/button";
import { Input } from "@azmr/ui/input";
import { type FormEvent, useId, useRef, useState } from "react";

const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export default function FormValidation() {
  const id = useId();
  const input = useRef<HTMLInputElement>(null);
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [sent, setSent] = useState(false);

  function submit(event: FormEvent) {
    event.preventDefault();
    if (!EMAIL.test(email)) {
      setSent(false);
      setError("Enter an email address like aroha@example.co.nz.");
      // Put the cursor on the problem, so nobody has to hunt for it.
      input.current?.focus();
      return;
    }
    setError(null);
    setSent(true);
  }

  return (
    // noValidate turns off the browser's own bubbles, so the message below is the only one.
    <form
      onSubmit={submit}
      noValidate
      aria-label="Subscribe"
      className="flex max-w-sm flex-col gap-3"
    >
      <label htmlFor={`${id}-email`} className="text-sm font-medium">
        Email
      </label>
      <Input
        id={`${id}-email`}
        ref={input}
        type="email"
        autoComplete="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        // The red border is not enough on its own: aria-invalid says it, the message explains it,
        // and aria-describedby ties the two together for a screen reader.
        aria-invalid={error ? "true" : undefined}
        aria-describedby={error ? `${id}-error` : undefined}
      />
      {error && (
        <p id={`${id}-error`} className="text-sm text-destructive">
          {error}
        </p>
      )}
      <Button type="submit" className="self-start">
        Subscribe
      </Button>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {sent ? `Subscribed ${email}.` : ""}
      </p>
    </form>
  );
}
```

## The pattern

1. Put `noValidate` on the `form`, so the browser's own pop-ups do not compete with yours.
2. On submit, call `preventDefault()`, check the values and store a message for each problem.
3. For an invalid [Input](https://platform.azmara.io/docs/components/input.md), set `aria-invalid="true"` and `aria-describedby` to the id of its message. Only set them while there is an error.
4. Move focus to the first invalid field. A keyboard user is left exactly where the problem is, and a screen reader reads the label, the state and the message.
5. Write the message as the fix ("Enter an email address like <aroha@example.co.nz>."), not the rule ("Invalid format").

Do not rely on colour. The red border is a reinforcement: `aria-invalid` and the text carry the meaning.

For a whole form with several fields, an error summary and a saving state, see the [settings page example](https://platform.azmara.io/docs/examples/settings-page.md).
