principletypescriptreactMajor
Form accessibility — labels, ARIA, and error announcements
Viewed 0 times
form accessibilityaria-describedbyaria-invalidrole alertlabel htmlForuseIdscreen readera11y forms
Problem
Forms with visually positioned labels, placeholder-only fields, and errors displayed only visually are inaccessible to screen reader users. Common mistakes include using placeholder as a label substitute, missing error associations, and unlabelled icon buttons.
Solution
Associate labels, use aria-describedby for errors, and use role=alert for dynamic messages:
import { useId } from 'react';
function AccessibleEmailField({
error,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & { error?: string }) {
const id = useId();
const errorId =
return (
<div>
{/ Explicit label — not placeholder /}
<label htmlFor={id}>Email address</label>
<input
id={id}
type="email"
aria-describedby={error ? errorId : undefined}
aria-invalid={error ? 'true' : undefined}
{...props}
/>
{/ role=alert announces the error when it appears /}
{error && (
<span id={errorId} role="alert">
{error}
</span>
)}
</div>
);
}
// For form-level errors:
function FormErrorSummary({ errors }: { errors: string[] }) {
if (!errors.length) return null;
return (
<div role="alert" aria-live="assertive">
<p>Please correct the following:</p>
<ul>{errors.map((e, i) => <li key={i}>{e}</li>)}</ul>
</div>
);
}
import { useId } from 'react';
function AccessibleEmailField({
error,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & { error?: string }) {
const id = useId();
const errorId =
${id}-error;return (
<div>
{/ Explicit label — not placeholder /}
<label htmlFor={id}>Email address</label>
<input
id={id}
type="email"
aria-describedby={error ? errorId : undefined}
aria-invalid={error ? 'true' : undefined}
{...props}
/>
{/ role=alert announces the error when it appears /}
{error && (
<span id={errorId} role="alert">
{error}
</span>
)}
</div>
);
}
// For form-level errors:
function FormErrorSummary({ errors }: { errors: string[] }) {
if (!errors.length) return null;
return (
<div role="alert" aria-live="assertive">
<p>Please correct the following:</p>
<ul>{errors.map((e, i) => <li key={i}>{e}</li>)}</ul>
</div>
);
}
Why
Screen readers announce elements with role=alert automatically when they appear in the DOM. aria-describedby links the input to its error message so the reader announces both the field name and the error on focus. aria-invalid signals to the reader that the field has a validation error.
Gotchas
- placeholder text disappears when the user types — never rely on it as the sole label
- Visually hidden labels (via CSS) are fine — label elements do not need to be visible, just present in the DOM
- useId() generates a stable, unique ID per component instance — use it instead of manual id strings to avoid duplicates
- role=alert fires immediately; aria-live=polite waits for the user to finish interacting before announcing
Revisions (0)
No revisions yet.