WCAG 3.3.1 · Level A · WCAG 2.0
Error Identification
Input errors must be identified in text — not just a red border. Describe what field failed and why.
- Principle
- Understandable
- Guideline
- Input Assistance
- Level
- A
- Added in
- WCAG 2.0
What it really means
When a form input error is detected, the error must be identified and described to the user in text. A red border alone is not enough — colour is not perceivable to everyone, and a border says "something is wrong" without saying what.
The rule doesn't mandate a specific pattern — inline message, error summary, or both work — but the error must be programmatically tied to the input, and its message must be plain text the user can read.
Who it helps
- Blind screen-reader users — who cannot see the border colour.
- Low-vision and colour-blind users — who may see the border but cannot distinguish red from grey.
- Cognitive-disabled users — who need explicit text to understand what went wrong.
- Everyone under pressure — tired, distracted, on mobile. Plain language wins.
How to test
- Submit a form with deliberately bad input. Every error should be:
- Described in text.
- Linked to its input via
aria-describedbyoraria-errormessage. - Announced by a screen reader without user action (usually via
aria-liveor focus move).
- Run axe DevTools —
aria-valid-attr-valueandaria-required-attrcatch the wiring,labelcatches missing labels. - Turn off CSS. All error messages should still be visible.
A failing pattern
// Red border only — screen readers know nothing.
<input
type="email"
className={invalid ? "border-red-500" : "border-neutral-300"}
/>
// Error in a toast that disappears in 3 seconds — missed by AT users.
if (invalid) toast.error("Invalid email");A passing pattern
Inline error message associated with the input:
function EmailField() {
const [value, setValue] = useState("");
const [error, setError] = useState<string | null>(null);
return (
<div>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
autoComplete="email"
value={value}
aria-invalid={error !== null}
aria-describedby={error ? "email-error" : undefined}
onChange={(event) => setValue(event.target.value)}
onBlur={() => setError(validateEmail(value))}
/>
{error && (
<p id="email-error" role="alert" className="text-(--danger)">
{error}
</p>
)}
</div>
);
}The key wiring:
aria-invalid="true"tells AT the input is in an error state.aria-describedby="email-error"pulls the message into the input's accessible description so AT reads it after the label.role="alert"on the message element announces it as it appears. Use this carefully — for form-wide feedback, a single error summary at the top with focus moved to it is often calmer.
Error summary pattern (GOV.UK):
On submit, collect all errors into a named region at the top of the form, move focus there, and link each item to the failing input.
{errors.length > 0 && (
<div
ref={summaryRef}
tabIndex={-1}
role="alert"
aria-labelledby="error-summary-title"
>
<h2 id="error-summary-title">There is a problem</h2>
<ul>
{errors.map((err) => (
<li key={err.field}>
<a href={`#${err.field}`}>{err.message}</a>
</li>
))}
</ul>
</div>
)}Notes and edge cases
aria-errormessageis the newer property that explicitly names the error text. Support across screen readers improved through 2023–2025;aria-describedbystill has broader support.- Don't
aria-live="assertive"on every error message — it cuts off other announcements. Usepolitefor most form feedback. - Don't rely on
requiredattribute alone to communicate required fields. Mark them visibly ("(required)") — not just asterisks.
Related rules
- WCAG 3.3.2 Labels or Instructions.
- WCAG 3.3.3 Error Suggestion — AA rule that requires suggestions when feasible.
- axe: aria-valid-attr-value.
- axe: label.
- axe: form-field-multiple-labels.