React: From Components to Production · Forms and Validation

Accessible Forms

Labels, errors, focus management and keyboard support for every user.

Every input needs a programmatic label (<label htmlFor> or wrapping label). Connect error messages with aria-describedby and mark invalid fields with aria-invalid. On a failed submit, move focus to the first invalid field or an error summary. Use native elements (<button>, <select>) for built-in keyboard support, group related controls with <fieldset>/<legend>, and use useId() to generate stable IDs.

import { useId } from "react";

export function Field({ label, error, ...input }: { label: string; error?: string } & React.ComponentProps<"input">) {
  const id = useId();
  const errorId = `${id}-error`;
  return (
    <div className="field">
      <label htmlFor={id}>{label}</label>
      <input id={id} aria-invalid={!!error} aria-describedby={error ? errorId : undefined} {...input} />
      {error && <p id={errorId} className="field-error">{error}</p>}
    </div>
  );
}

Common pitfalls

  • Placeholder text used as the only label.
  • Clickable <div>s instead of buttons (no keyboard or screen reader support).
  • Error messages conveyed only by red color.

Key points

  • Every input has a label
  • Link errors with aria-describedby
  • Native elements first; manage focus on errors

Further reading: W3C WAI: Forms tutorial

Practice questions

Why is a placeholder not a substitute for a label?

Answer

Placeholders disappear when typing, often have low contrast, and aren't reliably announced as labels by assistive technologies.

What does useId solve?

Answer

It generates unique, stable IDs that match between server and client rendering, for linking labels, descriptions and inputs without collisions.

Open in the app to mark lessons complete, take notes and track your progress.