WCAG 3.3.3 Error Suggestion Patterns
WCAG 2.2 Success Criterion 3.3.3 (Level AA) requires that when an input error is detected and a correction is known, the suggestion is provided to the user — and this recipe is a catalogue of patterns plus implementation that turns vague rejections into actionable, accessible guidance.
When to Use This Recipe
Use this once a form already identifies its errors (the Level A baseline in WCAG 3.3.1 error identification checklist) and you need to reach Level AA by telling the user how to fix each one. It applies to any field where the system can infer a correction: a missing value, a malformed email, an out-of-range number, a format mismatch, or a likely typo. It builds on the house pattern — <form novalidate> with custom rendered messages — and is the practical implementation of the suggestion half of the WCAG 2.2 form compliance checklists.
The 3.3.3 Checklist
Minimal Working Implementation
A single function maps each ValidityState failure to a corrective suggestion, reading the field’s own constraints so the message stays accurate as attributes change.
// Produce a corrective SUGGESTION (3.3.3), reading the field's constraints
// so the message reflects the actual min/max/pattern in the markup.
export function suggestCorrection(input: HTMLInputElement): string {
const v = input.validity;
const name = fieldName(input);
if (v.valueMissing) {
return `Enter your ${name}.`;
}
if (v.typeMismatch && input.type === 'email') {
return `Use the format name@example.com for your ${name}.`;
}
if (v.typeMismatch && input.type === 'url') {
return `Include https:// at the start of the ${name}.`;
}
if (v.rangeUnderflow || v.rangeOverflow) {
return `Enter a value between ${input.min || '0'} and ${input.max} for ${name}.`;
}
if (v.tooShort) {
return `Use at least ${input.minLength} characters for your ${name}.`;
}
if (v.patternMismatch) {
// The title attribute is the author's human-readable format hint.
return input.title || `Match the required format for your ${name}.`;
}
return input.validationMessage;
}
function fieldName(input: HTMLInputElement): string {
return input.labels?.[0]?.textContent?.trim().toLowerCase() ?? input.name;
}
Reading input.min, input.max, input.minLength, and input.title means the suggestion always matches the constraints declared in the markup — when an author changes max="99" to max="50", the message updates with no code edit. This derives directly from the Constraint Validation API Deep Dive flags.
The “Did You Mean…?” Typo Suggestion
For email domains, a small lookup against common misspellings turns a rejection into a one-tap fix — the most user-friendly form of 3.3.3.
const COMMON_DOMAINS = ['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com'];
// Suggest the nearest common domain when the typed one is a likely typo.
export function suggestEmailDomain(email: string): string | null {
const at = email.lastIndexOf('@');
if (at === -1) return null;
const [local, domain] = [email.slice(0, at), email.slice(at + 1)];
for (const candidate of COMMON_DOMAINS) {
if (domain !== candidate && levenshtein(domain, candidate) <= 2) {
return `${local}@${candidate}`;
}
}
return null;
}
function levenshtein(a: string, b: string): number {
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
for (let j = 0; j <= b.length; j++) d[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
}
}
return d[a.length][b.length];
}
Render the suggestion as an actionable button inside the associated message so keyboard and screen-reader users can accept it:
function renderSuggestion(input: HTMLInputElement, correction: string): void {
const error = document.getElementById(`${input.id}-error`)!;
error.textContent = '';
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = `Did you mean ${correction}?`;
btn.addEventListener('click', () => {
input.value = correction;
input.setCustomValidity(''); // clear so the field re-validates clean
error.textContent = '';
input.setAttribute('aria-invalid', 'false');
input.focus();
});
error.append(btn);
}
The setCustomValidity('') reset after accepting the suggestion is the lifecycle step from how to use setCustomValidity correctly — skipping it leaves the field permanently invalid.
Option Reference
| Failure | Suggestion pattern | Source of the detail |
|---|---|---|
valueMissing |
“Enter your <name>.” |
Field label |
typeMismatch (email/url) |
Name the format | Input type |
rangeUnderflow / rangeOverflow |
State allowed range | input.min / input.max |
tooShort |
State minimum length | input.minLength |
patternMismatch |
Echo the title hint |
input.title |
| Likely domain typo | “Did you mean …?” button | Levenshtein lookup |
Verification Steps
- Trigger each constraint and confirm the message names the correction, not just “invalid”.
- Type
name@gmial.comand confirm a “Did you mean name@gmail.com?” button appears and, when activated, fixes the value and clears the error. - With a screen reader, confirm the suggestion is announced and the accept button is reachable and labelled.
- Assert it in Playwright:
await expect(page.getByRole('alert')).toHaveText(/use the format name@example.com/i), as in testing form error messages with Playwright.
Edge Cases & Failure Modes
Suggestion leaks sensitive information
“That email is already registered” can reveal account existence where policy forbids it. Where required, suggest a generic next step (“Try signing in instead”) rather than confirming the value — coordinate this with your asynchronous server checks.
Range suggestion shows a blank bound
If a field has max but no min, input.min is '' and the message reads “between and 99”. Default the missing bound, as the implementation does with input.min || '0', or branch on which bound is present.
Accepting a suggestion leaves the field invalid
Setting input.value without clearing a prior setCustomValidity() string keeps customError true. Always reset with setCustomValidity('') after applying the correction, as renderSuggestion does.
Frequently Asked Questions
What is the difference between SC 3.3.1 and 3.3.3?
3.3.1 (Level A) requires identifying and describing the error. 3.3.3 (Level AA) goes further: when a correction is known, it must be suggested. A message like "Email is invalid" satisfies 3.3.1; "Use the format name@example.com" satisfies 3.3.3.
Do I have to offer a one-tap "Did you mean…?" fix?
No. 3.3.3 requires that you suggest the correction in text; an actionable button is an enhancement, not a requirement. A plain message naming the expected format conforms. The button simply lowers the effort to apply the suggestion.
When should a suggestion be withheld?
When the suggestion would expose security-sensitive information, such as confirming whether an account or email exists. WCAG explicitly exempts cases where revealing the correction would jeopardize security. Offer a generic, non-revealing next step instead.
Related Guides
- WCAG 2.2 Form Compliance Checklists — the full set of form-relevant success criteria
- WCAG 3.3.1 Error Identification Checklist — the Level A baseline this builds on
- How to Use setCustomValidity Correctly — clear the message after applying a suggestion
- Inline Error Messaging Strategies — where and how to place the suggested correction