Design error states and empty states that are perceivable, understandable, and actionable for all users — including those with cognitive disabilities, low vision, and screen readers.
Design error states and empty states that are perceivable, understandable, and actionable for all users — including those with cognitive disabilities, low vision, and screen readers.
Four WCAG success criteria directly govern error design:
3.3.1 Error Identification (Level A): If an input error is automatically detected, the item that is in error is identified and the error is described to the user in text. Color alone is insufficient.
3.3.2 Labels or Instructions (Level A): Labels or instructions are provided when content requires user input. (This governs hint text before errors occur.)
3.3.3 Error Suggestion (Level AA): If an input error is automatically detected and suggestions for correction are known, then the suggestion is provided to the user unless this would jeopardize security. (This requires actionable correction advice.)
3.3.4 Error Prevention (Level AA): For legal, financial, or data-deletion operations, submissions are reversible, checked, and/or confirmed before processing. (This governs confirmation dialogs and review steps.)
All four must be satisfied for full AA compliance. Most design systems satisfy 3.3.1 (error in text) but miss 3.3.3 (specific correction suggestion) and 3.3.4 (confirmation for destructive actions).
Error states require at least two visual indicators — color is always one, and at least one more is required:
1/* Error state: color + border width + icon + text */
2.form-field--error {
3 /* Indicator 1: color change */
4 --field-border-color: var(--color-status-error);
5
6 /* Indicator 2: border width increase */
7 --field-border-width: 2px;
8}
9
10/* The error message text is indicator 3 */
11.field-error-message {
12 color: var(--color-status-error);
13 display: flex;
14 align-items: center;
15 gap: 0.5rem;
16 font-size: 0.875rem;
17 margin-top: 0.25rem;
18}
19
20.field-error-message::before {
21 /* Indicator 4 (bonus): warning icon */
22 content: '';
23 display: block;
24 width: 16px;
25 height: 16px;
26 background: url("data:image/svg+xml,...") center/contain no-repeat;
27 /* The icon is decorative — the text carries the meaning */
28 flex-shrink: 0;
29}For server-side errors at the page level, use a summary error banner at the top of the form that lists all errors as links:
1<!-- Page-level error summary -->
2<div role="alert" aria-labelledby="error-summary-heading">
3 <h2 id="error-summary-heading">There are 2 errors in the form</h2>
4 <ul>
5 <li><a href="#email">Email: Enter a valid email address</a></li>
6 <li><a href="#phone">Phone: Include your country code</a></li>
7 </ul>
8</div>Error messages must be in plain language, identify what is wrong, and explain how to fix it. Cognitive accessibility (plain language, concrete examples) is just as important as technical accessibility:
1// Error message quality guide
2const ERROR_MESSAGES = {
3 // BAD: Technical and vague
4 email_invalid: 'Invalid input',
5
6 // BAD: Identifies problem but gives no correction
7 email_format: 'Invalid email format',
8
9 // GOOD: Identifies problem + gives example + is plain language
10 email_good: 'Enter a valid email address. Example: [email protected]',
11
12 // BAD: Jargon
13 password_weak: 'Password entropy insufficient',
14
15 // GOOD: Plain language with specific requirements
16 password_good: 'Password must be 8-32 characters and include at least one uppercase letter and one number',
17
18 // BAD: Blame-placing tone
19 required_bad: 'You forgot to fill in this field',
20
21 // GOOD: Neutral, constructive tone
22 required_good: 'First name is required',
23
24 // BAD: Generic server error
25 server_bad: 'Something went wrong. Try again.',
26
27 // GOOD: Specific with recovery action
28 server_good: 'We could not save your changes. Check your internet connection and try again. If the problem persists, contact support.',
29};For file upload errors, be specific about what is allowed:
1BAD: "File type not supported"
2GOOD: "Upload a JPEG, PNG, or WebP image under 5MB. Your file is a PDF (8.2MB)."Inline validation errors that appear as users type must be announced to screen readers without moving focus:
1function ValidatedInput({ id, label, validate }: ValidatedInputProps) {
2 const [value, setValue] = useState('');
3 const [error, setError] = useState('');
4 const [touched, setTouched] = useState(false);
5
6 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
7 const newValue = e.target.value;
8 setValue(newValue);
9 // Validate on change only if already touched (blur happened)
10 if (touched) {
11 setError(validate(newValue) ?? '');
12 }
13 };
14
15 const handleBlur = () => {
16 setTouched(true);
17 setError(validate(value) ?? '');
18 };
19
20 return (
21 <div className="form-field">
22 <label htmlFor={id}>{label}</label>
23 <input
24 id={id}
25 value={value}
26 onChange={handleChange}
27 onBlur={handleBlur}
28 aria-invalid={error ? 'true' : undefined}
29 aria-describedby={error ? `${id}-error` : undefined}
30 />
31 {/* role='alert' announces immediately; aria-live='polite' waits for silence */}
32 {/* For inline validation: polite is less disruptive */}
33 <div
34 id={`${id}-error`}
35 role="status"
36 aria-live="polite"
37 aria-atomic="true"
38 className="field-error"
39 >
40 {error}
41 </div>
42 </div>
43 );
44}Do not validate on every keystroke — validate on blur (field loses focus) to avoid interrupting users mid-entry. Exception: password strength meters can update on keystroke as they provide positive reinforcement, not errors.
Empty states (empty inbox, empty search results, empty cart) are often overlooked in accessibility reviews but are critical for cognitive accessibility:
1// Accessible empty state pattern
2function EmptyState({
3 icon,
4 title,
5 description,
6 actionLabel,
7 onAction,
8}: EmptyStateProps) {
9 return (
10 <section
11 aria-labelledby="empty-state-heading"
12 className="empty-state"
13 >
14 {/* Icon: decorative, aria-hidden */}
15 <img
16 src={icon}
17 alt="" /* empty alt = decorative */
18 aria-hidden="true"
19 width="120"
20 height="120"
21 />
22
23 {/* Clear heading that names the state */}
24 <h2 id="empty-state-heading">{title}</h2>
25
26 {/* Description: what happened, why it's empty */}
27 <p>{description}</p>
28
29 {/* Primary action to resolve the empty state */}
30 {actionLabel && onAction && (
31 <button onClick={onAction} className="btn-primary">
32 {actionLabel}
33 </button>
34 )}
35 </section>
36 );
37}
38
39// Usage examples:
40<EmptyState
41 icon="/empty-search.svg"
42 title='No results for "react hooks"'
43 description="Try different keywords or check your spelling."
44 actionLabel="Clear search"
45 onAction={clearSearch}
46/>
47
48<EmptyState
49 icon="/empty-inbox.svg"
50 title="Your inbox is empty"
51 description="New messages from instructors and learners will appear here."
52 // No action needed — this is informational
53/>Empty states for search results must echo the search query (so users with memory impairments see what they searched for) and suggest next steps in plain language.
Audit your application's 5 most common error messages against the quality criteria: does each identify the problem, explain what is wrong, and suggest how to fix it with an example? Rewrite any that fail this test.
Your progress is saved in this browser, on this device only.