| applyTo | ** |
|---|---|
| description | Comprehensive web accessibility standards based on WCAG 2.2 AA, with 38+ anti-patterns, legal enforcement context (EAA, ADA Title II), WAI-ARIA patterns, and framework-specific fixes for modern web frameworks and libraries. |
Comprehensive accessibility rules for web application development. Every anti-pattern includes a severity classification, detection method, WCAG 2.2 reference, and corrective code examples.
Severity levels:
- CRITICAL — Users cannot access content at all. Must be fixed before merge.
- IMPORTANT — Significant barrier for assistive technology users. Fix in same sprint.
- SUGGESTION — Improves usability for assistive technology. Plan for a future iteration.
| Criterion | Level | Summary |
|---|---|---|
| 1.1.1 Non-text Content | A | All non-text content has a text alternative. Decorative images use alt="". |
| 1.2.1 Audio/Video-only | A | Provide transcript (audio) or text alternative (video). |
| 1.2.2 Captions (Prerecorded) | A | All prerecorded video has synchronized captions. |
| 1.3.1 Info and Relationships | A | Structure (headings, lists, tables, labels, landmarks) programmatically conveyed. |
| 1.3.2 Meaningful Sequence | A | DOM reading order matches visual order. |
| 1.3.3 Sensory Characteristics | A | Instructions don't rely solely on shape, size, position, or sound. |
| 1.3.4 Orientation | AA | Content not restricted to single orientation unless essential. |
| 1.3.5 Identify Input Purpose | AA | Input fields have autocomplete attributes for user data (name, email, tel). |
| 1.4.1 Use of Color | A | Color is not the only means of conveying info. |
| 1.4.3 Contrast (Minimum) | AA | Text: 4.5:1 normal, 3:1 large (18pt / 14pt bold). |
| 1.4.4 Resize Text | AA | Text resizable to 200% without loss of content. |
| 1.4.10 Reflow | AA | Content reflows at 320px CSS width (no horizontal scroll). |
| 1.4.11 Non-text Contrast | AA | UI components and graphics: 3:1 against adjacent colors. |
| 1.4.12 Text Spacing | AA | No loss of content with overridden line-height (1.5x), spacing. |
| 1.4.13 Content on Hover/Focus | AA | Tooltips: dismissible, hoverable, persistent. |
| Criterion | Level | Summary |
|---|---|---|
| 2.1.1 Keyboard | A | All functionality operable via keyboard. |
| 2.1.2 No Keyboard Trap | A | User can navigate away from any component using keyboard. |
| 2.2.1 Timing Adjustable | A | Time limits can be extended or disabled. |
| 2.2.2 Pause, Stop, Hide | A | Auto-updating content can be paused. |
| 2.3.1 Three Flashes | A | No content flashes more than 3 times per second. |
| 2.4.1 Bypass Blocks | A | Skip link to bypass repeated navigation. |
| 2.4.2 Page Titled | A | Pages have descriptive <title>. |
| 2.4.3 Focus Order | A | Focus order preserves meaning and operability. |
| 2.4.4 Link Purpose | A | Link purpose determinable from text or context. |
| 2.4.6 Headings and Labels | AA | Headings and labels describe topic or purpose. |
| 2.4.7 Focus Visible | AA | Keyboard focus indicator is visible. |
| 2.4.11 Focus Not Obscured | AA | Focused element not entirely hidden by sticky headers/footers. (New in 2.2) |
| 2.5.1 Pointer Gestures | A | Multi-point gestures have single-pointer alternative. |
| 2.5.2 Pointer Cancellation | A | Activation on up-event, not down-event. |
| 2.5.3 Label in Name | A | Accessible name contains the visible label text. |
| 2.5.4 Motion Actuation | A | Device motion has UI alternative and can be disabled. |
| 2.5.7 Dragging Movements | AA | Drag-and-drop has click/tap alternative. (New in 2.2) |
| 2.5.8 Target Size (Minimum) | AA | Touch targets at least 24x24 CSS px. (New in 2.2) |
| Criterion | Level | Summary |
|---|---|---|
| 3.1.1 Language of Page | A | <html lang="..."> set correctly. |
| 3.1.2 Language of Parts | AA | Content in different language marked with lang attribute. |
| 3.2.1 On Focus | A | Focus doesn't trigger unexpected context change. |
| 3.2.2 On Input | A | Changing input doesn't auto-trigger unexpected context change. |
| 3.2.6 Consistent Help | A | Help mechanisms in same relative order across pages. (New in 2.2) |
| 3.3.1 Error Identification | A | Errors described to user in text. |
| 3.3.2 Labels or Instructions | A | Labels or instructions provided for user input. |
| 3.3.3 Error Suggestion | AA | Suggest corrections for detected errors. |
| 3.3.4 Error Prevention | AA | Submissions are reversible, checked, or confirmed. |
| 3.3.7 Redundant Entry | A | Don't re-ask for info already provided in same process. (New in 2.2) |
| 3.3.8 Accessible Authentication (Minimum) | AA | No cognitive function test (puzzle CAPTCHA). Allow paste and autofill. (New in 2.2) |
| Criterion | Level | Summary |
|---|---|---|
| 4.1.2 Name, Role, Value | A | All UI components have accessible name, role, and state. |
| 4.1.3 Status Messages | AA | Status messages announced by screen readers without receiving focus. |
Note: 4.1.1 Parsing is obsolete in WCAG 2.2 (always satisfied). Issues it covered are now addressed by 1.3.1 and 4.1.2.
New AAA criteria in 2.2 (not required for AA, but recommended): 2.4.12 Focus Not Obscured (Enhanced), 2.4.13 Focus Appearance, 3.3.9 Accessible Authentication (Enhanced).
Looking ahead: WCAG 3.0 (W3C Accessibility Guidelines) is in Working Draft (March 2026). It replaces pass/fail with Bronze/Silver/Gold conformance and "Outcomes" instead of "Success Criteria." It is NOT yet a standard — continue targeting WCAG 2.2 AA.
- European Accessibility Act (EAA): Enforced since June 2025 across all 27 EU member states. Applies to digital products and services. Fines up to EUR 3 million. References EN 301 549 (maps to WCAG 2.1 AA).
- ADA Title II (US): Digital accessibility rule effective April 2026 for state/local governments serving 50,000+ people (April 2027 for smaller entities). Requires WCAG 2.1 AA.
- Section 508 (US Federal): References WCAG 2.0 AA (refresh to 2.1/2.2 expected).
Target: WCAG 2.2 AA covers all current legal requirements (superset of 2.1 AA and 2.0 AA).
- Prefer native HTML — Use
<button>not<div role="button">. Native elements have built-in keyboard, focus, and semantics. - Don't change native semantics — Don't add
role="heading"to a<button>. Use the correct element. - All ARIA controls must be keyboard operable — If
role="button", handle Enter and Space key events. - Don't use
aria-hidden="true"on focusable elements — Hidden from assistive tech but still focusable creates a "ghost" element. - All interactive elements need an accessible name — Via label,
aria-label,aria-labelledby, or visible text content.
- Severity: CRITICAL
- Detection:
<htmlwithoutlang= - WCAG: 3.1.1 (A)
<!-- BAD -->
<html>
<!-- GOOD -->
<html lang="en">Next.js: Set in app/layout.tsx. Angular: Set in src/index.html. Vue/Nuxt: Set in app.vue or nuxt.config.
- Severity: SUGGESTION
- Detection: Multiple
<h1>elements that make the page heading structure unclear - WCAG: Best practice (supports 1.3.1)
Prefer one <h1> per page representing the main topic. Use <h2> for sections. Multiple <h1> elements are not a strict WCAG violation but can confuse screen reader navigation.
- Severity: IMPORTANT
- Detection:
<h1>followed by<h3>(skipping<h2>) - WCAG: 1.3.1 (A)
Maintain logical nesting: h1 > h2 > h3 > h4. Style headings with CSS, not by choosing a different heading level.
- Severity: IMPORTANT
- Detection: Pages using only
<div>without<nav>,<main>,<header>,<footer> - WCAG: 1.3.1 (A), 2.4.1 (A)
<!-- GOOD -->
<header>...</header>
<nav aria-label="Main">...</nav>
<main>...</main>
<footer>...</footer>- Severity: IMPORTANT
- Detection:
<table>without<th>,<caption>, orrole="presentation" - WCAG: 1.3.1 (A)
Use CSS Grid/Flexbox for layout. If table must be used for layout, add role="presentation".
- Severity: CRITICAL
- Detection:
<table>with data rows but no<th>elements - WCAG: 1.3.1 (A)
<!-- GOOD -->
<table>
<caption>User list</caption>
<thead>
<tr><th scope="col">Name</th><th scope="col">Email</th></tr>
</thead>
<tbody>
<tr><td>Alice</td><td>alice@example.com</td></tr>
</tbody>
</table>- Severity: IMPORTANT
- Detection:
>click here<|>read more<|>learn more<|>here<|>more<|>link< - WCAG: 2.4.4 (A)
<!-- BAD -->
<a href="/pricing">Click here</a>
<!-- GOOD -->
<a href="/pricing">View pricing plans</a>- Severity: CRITICAL
- Detection:
<div.*(?:onClick|@click|\(click\)) - WCAG: 4.1.2 (A)
// BAD — not focusable, no role, no keyboard support
<div onClick={handleClick}>Submit</div>
// GOOD
<button onClick={handleClick}>Submit</button>- Severity: SUGGESTION
- Detection:
<button.*role="button"|<nav.*role="navigation"|<a.*role="link" - WCAG: ARIA Rule 2
Remove redundant ARIA. <button> already has role="button".
A2: aria-hidden="true" on Focusable Element
- Severity: CRITICAL
- Detection:
aria-hidden="true"on focusable elements (button, input, a, [tabindex]) - WCAG: ARIA Rule 4
Use inert attribute or remove from tab order entirely.
- Severity: CRITICAL
- Detection:
role="tab"withoutaria-selected,role="checkbox"withoutaria-checked - WCAG: 4.1.2 (A)
Required per role: tab needs aria-selected/aria-controls; combobox needs aria-expanded/aria-controls; slider needs aria-valuemin/aria-valuemax/aria-valuenow; checkbox needs aria-checked.
- Severity: CRITICAL
- Detection:
role="[^"]*"with non-existent values - WCAG: 4.1.2 (A)
Invalid roles are ignored by assistive technology. Common mistakes: role="input", role="text", misspellings.
- Severity: IMPORTANT
- Detection:
role="button"on<div>,role="checkbox"on<div> - WCAG: ARIA Rule 1
<!-- BAD — requires manual keyboard, focus, and state management -->
<div role="checkbox" aria-checked="false" tabindex="0">Accept terms</div>
<!-- GOOD — all behavior built-in -->
<label><input type="checkbox" /> Accept terms</label>- Severity: CRITICAL
- Detection:
<buttonwith SVG/icon child and no text oraria-label - WCAG: 4.1.2 (A)
<!-- GOOD -->
<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>- Severity: IMPORTANT
- Detection:
role="presentation"on interactive elements - WCAG: ARIA Rule 4
- Severity: IMPORTANT
- Detection: Toast/notification components without
role="alert",role="status", oraria-live - WCAG: 4.1.3 (AA)
<!-- GOOD — content announced when injected -->
<div role="status" aria-live="polite">Item saved successfully</div>
<!-- Use role="alert" (assertive) for errors -->
<div role="alert">Failed to save. Please try again.</div>- Severity: CRITICAL
- Detection:
(?:onClick|@click|\(click\))on<div>or<span>without keyboard handler - WCAG: 2.1.1 (A)
Use <button> instead. If div is required: add role="button", tabIndex={0}, and handle Enter/Space.
- Severity: CRITICAL
- Detection:
(?:tabindex="[1-9]\d*"|tabIndex=\{[1-9]\d*\}) - WCAG: 2.4.3 (A)
Only use tabindex="0" (add to tab order) and tabindex="-1" (programmatic focus only).
- Severity: CRITICAL
- Detection: Modal/overlay without Escape key handler or focus trapping
- WCAG: 2.1.2 (A)
Use native <dialog> with showModal() — it provides focus trapping, Escape-to-close, and focus return automatically. Use inert attribute on background content to prevent interaction outside the dialog (96%+ browser support). If custom implementation is needed: trap Tab within the dialog, close on Escape, return focus to the trigger element on close.
- Severity: IMPORTANT
- Detection: No skip link as first focusable element
- WCAG: 2.4.1 (A)
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content" tabindex="-1">...</main>.skip-link { position: absolute; top: -40px; left: 0; padding: 8px 16px; background: #000; color: #fff; z-index: 100; }
.skip-link:focus { top: 0; }- Severity: CRITICAL
- Detection:
outline:\s*none|outline:\s*0\bwithout:focus-visiblereplacement - WCAG: 2.4.7 (AA)
/* GOOD */
button:focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; }- Severity: IMPORTANT
- Detection:
onMouseOver|onMouseEnter|@mouseenterwithout keyboard equivalent - WCAG: 2.1.1 (A)
Pair hover with focus events. Use onFocus/onBlur alongside onMouseEnter/onMouseLeave.
- Severity: IMPORTANT
- Detection: Dialog close without restoring focus to trigger
- WCAG: 2.4.3 (A)
Store reference to trigger element. On modal close, call triggerElement.focus().
- Severity: CRITICAL
- Detection:
<input|<select|<textareawithout<label>,aria-label, oraria-labelledby - WCAG: 1.3.1 (A), 3.3.2 (A)
<!-- GOOD -->
<label for="email">Email address</label>
<input id="email" type="email" placeholder="you@example.com" />- Severity: CRITICAL
- Detection: Error elements near inputs without
aria-describedby - WCAG: 3.3.1 (A)
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true" />
<span id="email-error" class="error">Invalid email format</span>- Severity: IMPORTANT
- Detection:
requiredor*withoutaria-required="true"or HTMLrequired - WCAG: 3.3.2 (A), 1.4.1 (A)
<label for="name">Name <span aria-hidden="true">*</span></label>
<input id="name" type="text" required />
<p class="form-note">Fields marked * are required</p>- Severity: IMPORTANT
- Detection: Form submit handler without focus management on validation failure
- WCAG: 3.3.1 (A)
On submit failure, focus the first invalid field or show and focus an error summary.
- Severity: IMPORTANT
- Detection: Puzzle/image CAPTCHAs without fallback; password fields with
autocomplete='off'or paste-blocking JavaScript - WCAG: 3.3.8 (AA)
Use reCAPTCHA v3 (invisible), hCaptcha accessibility mode, or alternative authentication. Never block paste or autofill on password fields — this violates WCAG 3.3.8.
- Severity: IMPORTANT
- Detection:
placeholder=without accompanying<label>,aria-label, oraria-labelledby - WCAG: 3.3.2 (A)
Always pair placeholder with a visible <label>. Placeholder is a hint, not a label.
- Severity: CRITICAL
- Detection: Text color combinations below 4.5:1 (normal) or 3:1 (large)
- WCAG: 1.4.3 (AA)
/* BAD — #999 on #fff is ~2.5:1 */
.text { color: #999; background: #fff; }
/* GOOD — #595959 on #fff is 7.0:1 */
.text { color: #595959; background: #fff; }- Severity: CRITICAL
- Detection: Error/success states distinguished only by color
- WCAG: 1.4.1 (A)
Add a secondary indicator: icon, text, pattern, underline, or border.
- Severity: IMPORTANT
- Detection:
font-size:\s*[0-9]*px(excluding root/base) - WCAG: 1.4.4 (AA)
Use rem or em for font sizes. Base font can be px, but content fonts should be relative.
- Severity: IMPORTANT
- Detection: Fixed-width containers, horizontal scroll at narrow widths
- WCAG: 1.4.10 (AA)
Use responsive layouts (Grid, Flexbox). Test at 320px CSS width. Avoid fixed-width containers.
- Severity: SUGGESTION
- Detection:
animation:|transition:withoutprefers-reduced-motionmedia query - WCAG: 2.3.3 (AAA)
Best practice and AAA enhancement. Gate non-essential animations behind prefers-reduced-motion so users who request less motion are not forced to experience interaction-triggered effects.
@media (prefers-reduced-motion: no-preference) {
.card { transition: transform 0.3s ease; }
.card:hover { transform: scale(1.05); }
}- Severity: CRITICAL
- Detection:
<img|<Imagewithoutalt=attribute - WCAG: 1.1.1 (A)
Alt text decision tree: decorative = alt=""; contains text = include that text; functional = describe action; informational = describe content.
- Severity: SUGGESTION
- Detection: Decorative images with meaningful alt text
- WCAG: 1.1.1 (A)
Use alt="" for decorative images. Add aria-hidden="true" for decorative SVGs.
- Severity: CRITICAL
- Detection:
<videowithout<track kind="captions"> - WCAG: 1.2.2 (A)
<video src="/tutorial.mp4" controls>
<track kind="captions" src="/tutorial-en.vtt" srclang="en" label="English" default />
</video>- Severity: IMPORTANT
- Detection:
autoplayattribute withoutmuted - WCAG: 1.4.2 (A)
Never autoplay audio. If video autoplays, start muted with controls.
- Severity: IMPORTANT
- Detection:
<label.*for="in JSX (should behtmlFor) - WCAG: 1.3.1 (A), 3.3.2 (A)
- Severity: IMPORTANT
- Detection: Navigation without focus management or live region
- WCAG: 4.1.3 (AA)
After route change, focus the main heading or announce the new page title via a live region. Next.js includes a built-in route announcer (since v13) that reads document.title, then <h1>, then pathname. Ensure every page has a unique <title>.
- Severity: SUGGESTION
- Detection:
<>...</>root with conditional rendering causing DOM restructuring - WCAG: 2.4.3 (A)
Use key prop to preserve DOM identity, or manually restore focus with useRef + useEffect.
- Severity: IMPORTANT
- Detection: Rich text rendering without accessibility validation
- WCAG: 1.3.1 (A)
Sanitize and validate injected HTML for heading hierarchy, alt text, and ARIA structure.
- Severity: CRITICAL
- Detection:
(click)on<div>or<span>withoutrole=,tabindex,(keydown) - WCAG: 2.1.1 (A), 4.1.2 (A)
Use <button>. If div required: add role="button", tabindex="0", (keydown.enter), (keydown.space).
- Severity: IMPORTANT
- Detection: Modal components without
cdkTrapFocus - WCAG: 2.1.2 (A)
<div class="modal" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">...</div>Angular CDK's Dialog service handles focus trapping and restoration automatically.
- Severity: IMPORTANT
- Detection: Angular Router navigation without
LiveAnnouncer - WCAG: 4.1.3 (AA)
router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => {
liveAnnouncer.announce(titleService.getTitle(), 'polite');
});- Severity: IMPORTANT
- Detection: Forms showing errors without
[attr.aria-invalid]or[attr.aria-describedby] - WCAG: 3.3.1 (A), 3.3.3 (AA)
Bind [attr.aria-invalid] and [attr.aria-describedby] to form control state.
- Severity: CRITICAL
- Detection:
@clickon<div>or<span>withoutrole=,tabindex,@keydown - WCAG: 2.1.1 (A), 4.1.2 (A)
Use <button>. Or add role="button", tabindex="0", @keydown.enter, @keydown.space.prevent.
- Severity: IMPORTANT
- Detection:
v-iftoggling without managing focus vianextTick - WCAG: 2.4.3 (A)
<script setup>
import { ref, watch, nextTick } from 'vue';
const showPanel = ref(false);
const panel = ref(null);
watch(showPanel, async (val) => {
if (val) { await nextTick(); panel.value?.focus(); }
});
</script>- Severity: IMPORTANT
- Detection:
v-htmlrendering user or CMS content - WCAG: 1.3.1 (A)
Sanitize and validate HTML for heading hierarchy, alt text, and ARIA structure before injection.
| Key | Expected Behavior |
|---|---|
Tab |
Move focus to next focusable element in DOM order |
Shift+Tab |
Move focus to previous focusable element |
Enter |
Activate buttons and links |
Space |
Activate buttons, toggle checkboxes, select radio buttons |
Escape |
Close modals, dialogs, popovers, dropdowns |
Arrow Up/Down |
Navigate within menus, listboxes, radio groups, tabs |
Arrow Left/Right |
Navigate within tab bars, sliders, radio groups |
Home |
Move to first item in list, menu, or tab bar |
End |
Move to last item in list, menu, or tab bar |
| Widget | Tab enters | Internal nav | Activate | Exit |
|---|---|---|---|---|
| Tab bar | Focus active tab | Arrow Left/Right | automatic or Enter | Tab out |
| Menu | Focus first item | Arrow Up/Down | Enter | Escape |
| Dialog | Focus first element | Tab cycles within | Enter on buttons | Escape |
| Combobox | Focus input | Arrow Up/Down | Enter selects | Escape closes |
| Tree view | Focus first node | Arrow keys | Enter/Space | Tab out |
| Text Type | Minimum Ratio |
|---|---|
| Normal text (< 18pt / < 14pt bold) | 4.5:1 |
| Large text (>= 18pt / >= 14pt bold) | 3:1 |
| Incidental (disabled, decorative) | No requirement |
| Element | Minimum Ratio |
|---|---|
| UI components (borders, icons) | 3:1 against adjacent |
| Graphical objects | 3:1 against adjacent |
| Focus indicators | 3:1 against background |
- All images have appropriate alt text (descriptive or empty for decorative)
- Videos have synchronized captions
- Page uses semantic landmarks:
<header>,<nav>,<main>,<footer> - Headings follow logical hierarchy (h1 > h2 > h3, no gaps)
- Text contrast meets 4.5:1 (normal) / 3:1 (large)
- UI component contrast meets 3:1
- Information not conveyed by color alone
- Content reflows at 320px without horizontal scroll
-
<html lang="...">is set correctly - Text resizable to 200% without loss of content
- All functionality accessible via keyboard
- No keyboard traps (Escape closes overlays)
- Skip link provided as first focusable element
- Focus indicator visible on all interactive elements
- Focus order matches visual order
- Focus not obscured by sticky headers/footers
- Focus returned to trigger after modal close
- Touch targets at least 24x24 CSS px
- Animations respect
prefers-reduced-motion - No content flashes more than 3 times per second
- All form inputs have associated
<label>oraria-label - Error messages linked to inputs via
aria-describedby - Required fields indicated with
requiredoraria-required - Error summary or focus-on-first-error on submit failure
- No unexpected context changes on focus or input
- All interactive elements have accessible name, role, and state
- ARIA roles have required properties
- No
aria-hidden="true"on focusable elements - Dynamic content announced via live regions
- SPA route changes announced to screen readers
- No redundant ARIA on native HTML elements