Skip to content

Form Accessibility React

DodaTech 4 min read

title: "Form Accessibility in React" weight: 16 description: "Learn how to build accessible forms in React: labels linked to inputs, error messages with aria-describedby, live region announcements, validation with aria-invalid, and form state management." date: 2026-06-28 lastmod: 2026-06-28 tags: [accessibility, react]


Accessible forms in React require proper label associations via htmlFor or aria-label, inline error messages linked with aria-describedby, validation indicators with aria-invalid, and live region announcements for form submission feedback.

## What You'll Learn

You will build accessible forms in React, handle validation with accessible errors, manage form state accessibly, and ensure screen readers announce form updates.

## Why It Matters

Forms are critical user interactions. Inaccessible forms prevent users from completing registrations, purchases, and other essential tasks. React's state management makes it easy to create dynamic, accessible forms.

## Real-World Use

A React registration form shows errors inline but does not link them to inputs. Screen reader users cannot associate errors with fields. Adding aria-describedby and aria-invalid makes the form fully accessible.

## Accessible Form Structure

```mermaid
flowchart TD
  A[Form Component] --> B[Field Component]
  B --> C[label with htmlFor]
  B --> D[input with id]
  B --> E[error message]
  B --> F[aria-describedby]
  B --> G[aria-invalid]
  E --> F
  G --> E

Building Accessible Forms

Create field components that encapsulate label, input, and error handling.

function FormField({ label, name, type = 'text', error, ...props }) {
  const fieldId = `field-${name}`;
  const errorId = `error-${name}`;

  return (
    <div className="form-field">
      <label htmlFor={fieldId}>{label}</label>
      <input
        id={fieldId}
        name={name}
        type={type}
        aria-invalid={!!error}
        aria-describedby={error ? errorId : undefined}
        {...props}
      />
      {error && (
        <p id={errorId} role="alert" className="form-error">
          {error}
        </p>
      )}
    </div>
  );
}

function RegistrationForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = (name, value) => {
    switch (name) {
      case 'email':
        return value.includes('@') ? undefined : 'Please enter a valid email';
      case 'password':
        return value.length >= 8 ? undefined : 'Password must be at least 8 characters';
      default:
        return undefined;
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    setErrors(prev => ({ ...prev, [name]: validate(name, value) }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = {};
    Object.keys(formData).forEach(key => {
      const error = validate(key, formData[key]);
      if (error) newErrors[key] = error;
    });
    setErrors(newErrors);

    if (Object.keys(newErrors).length === 0) {
      announceSuccess('Registration submitted successfully');
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <FormField
        label="Email address"
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        error={errors.email}
        autoComplete="email"
      />
      <FormField
        label="Password"
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
        error={errors.password}
        autoComplete="new-password"
      />
      <button type="submit">Register</button>
    </form>
  );
}
// Live region for form submission
function announceSuccess(message) {
  const announcer = document.getElementById('form-announcer');
  if (announcer) {
    announcer.textContent = '';
    setTimeout(() => {
      announcer.textContent = message;
    }, 50);
  }
}

Common Mistakes

  • Using placeholder instead of label
  • Not linking errors to inputs with aria-describedby
  • Not using aria-invalid on invalid fields
  • Showing errors only in a summary at the top of the form
  • Clearing form state without announcing the change
  • Not using noValidate to prevent browser validation interference
  • Forgetting autocomplete attributes for browser autofill

Practice and Challenge

Practice 1: Create a reusable FormField component with accessibility. Practice 2: Add aria-describedby linking errors to inputs. Practice 3: Implement live region for form submission feedback. Practice 4: Add aria-invalid to invalid fields. Practice 5: Test the form with a screen reader.

Challenge: Build a multi-step registration form in React with accessibility at every step. Include: field-level validation with inline errors, step progress indicator with aria-current, focus management on step transitions, live region announcements for step changes, and a summary review step before final submission.

FAQ

How do I link a label to an input in React?

Use htmlFor on the label and id on the input. In React, htmlFor is equivalent to HTML's for attribute.

How do I announce form errors to screen readers?

Use role='alert' on error messages and aria-describedby on the input to link them.

Should I disable the submit button until the form is valid?

No. Disabled buttons are not focusable. Show errors on submit attempt instead.

How do I handle async validation accessibility?

Show a loading state with aria-busy and announce results with a live region.

What about form submission success?

Use a live region (aria-live='polite') to announce success messages.

Do I need noValidate on React forms?

Yes. Use noValidate to prevent browser validation from interfering with custom accessible validation.

Mini Project

Create a React form library with accessible components: FormField (text, email, password, textarea), SelectField, CheckboxGroup, RadioGroup, and SubmitButton. Each component should include proper labeling, error handling, aria attributes, and keyboard support. The library should include a demo form that passes axe-core testing.

What's Next

React Router Accessibility covers focus management in React Router navigation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro