Labels and ARIA Attributes

Every control needs an accessible name, and the cheapest one is a real <label htmlFor>. The React-specific problem is the id: a reusable <Field> can appear five times on a page, so a hard-coded id="email" gives five duplicates and four broken labels. useId solves it — unique per component instance and identical on server and client, so it survives hydration. Never use it as a list key.

ARIA fills the gaps HTML cannot express: aria-describedby attaches hint and error text without folding it into the name, and aria-invalid marks the field as failing. Both take ID references built from that same useId value. Pass undefined, not false or '', for an attribute that should not be emitted.

A field component that wires up its own label, hint and errorJavaScript
function Field({ label, hint, error, ...rest }) {
  const id = useId(), hintId = `${id}-hint`, errId = `${id}-err`;
  return <p>
    <label htmlFor={id}>{label}</label><br />
    <input id={id} aria-invalid={error ? true : undefined}
           aria-describedby={[hint && hintId, error && errId]
             .filter(Boolean).join(' ') || undefined} {...rest} />
    {hint && <span id={hintId}> {hint}</span>}
    {error && <span id={errId} style={{ color: '#b3261e' }}> {error}</span>}
  </p>;
}

Render two — one with a hint, one submitted empty — and both strings arrive as the field's description, apart from its name. Chromium 4,389 's tree prints the second field as textbox "Email" invalid=true desc="Enter a work email.", which is exactly what a screen reader reads out: the name first, the state, then the description.

aria-label on a visible control replaces that text for screen reader users and breaks voice control, which matches spoken words against what is on screen; reserve it for icon-only buttons, and mark the icon aria-hidden="true". role is a last resort: role="button" on a <div> promises keyboard behavior you must then write by hand. Name precedence runs aria-labelledby, aria-label, <label>, placeholder.