Web Accessibility (WCAG) Cheat Sheet
Covers ARIA roles and attributes, keyboard focus management, the WCAG POUR principles and conformance levels, and common accessibility checks.
ARIA Roles & Attributes
Communicating state and purpose to assistive tech.
<!-- Use native elements first; ARIA is a last resort --><button aria-label="Close dialog" onclick="closeDialog()">×</button><div role="alert" aria-live="assertive"> Form submitted successfully.</div><input type="text" aria-describedby="email-hint" aria-invalid="true" aria-required="true"/><span id="email-hint">We'll never share your email.</span><button aria-expanded="false" aria-controls="menu">Menu</button><ul id="menu" hidden>...</ul>
Keyboard Navigation & Focus
Making custom controls operable without a mouse.
<!-- Ensure custom interactive elements are keyboard-operable --><div role="button" tabindex="0" onkeydown="if(event.key==='Enter'||event.key===' ')activate()" onclick="activate()"> Custom Button</div>
Visible Focus & Skip Links
CSS that keeps keyboard focus visible and navigable.
/* Never remove focus outlines without a visible replacement */:focus-visible { outline: 2px solid #2684ff; outline-offset: 2px;}/* Skip link lets keyboard users bypass repeated navigation */.skip-link { position: absolute; left: -9999px;}.skip-link:focus { left: 0;}
WCAG POUR Principles & Levels
The four pillars WCAG success criteria are organized around.
- Perceivable- content must be presentable to users in ways they can perceive, e.g. alt text, captions, sufficient contrast
- Operable- UI components and navigation must be operable, e.g. full keyboard access, no seizure-inducing flashing
- Understandable- content and UI operation must be understandable, e.g. consistent navigation, clear labels and error messages
- Robust- content must work reliably with assistive technologies through valid markup and correct ARIA usage
- Level A/AA/AAA- WCAG's three conformance levels; AA is the common legal and industry baseline
- 4.5:1 contrast ratio- minimum contrast for normal text at WCAG AA (3:1 for large text)
Accessible Name Computation Order
How browsers resolve which text an assistive tech announces when multiple naming sources are present.
<!-- Precedence (highest wins): aria-labelledby > aria-label > native label/legend > placeholder/title --><label for="qty">Quantity</label><input id="qty" aria-label="Item quantity" placeholder="1" /><!-- announced name: "Item quantity" (aria-label beats the <label>) --><h2 id="billing-h">Billing Address</h2><section aria-labelledby="billing-h"> <!-- section's accessible name comes from the referenced heading --></section><!-- aria-labelledby can concatenate multiple ids, in DOM order given --><span id="lbl-a">Ship to</span><span id="lbl-b">Home</span><div role="group" aria-labelledby="lbl-a lbl-b">...</div><!-- accessible name: "Ship to Home" -->
Focus Trapping & Restoration in Dialogs
Keeping keyboard focus inside a modal and returning it correctly on close.
function trapFocus(dialog) { const focusable = dialog.querySelectorAll( 'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; const previouslyFocused = document.activeElement; dialog.addEventListener('keydown', (e) => { if (e.key === 'Escape') return closeDialog(); if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); first.focus(); return () => previouslyFocused.focus(); // call on close to restore focus}// Prefer the native <dialog> element when possible: showModal()// already traps focus and Escape-to-close for free.const dialog = document.querySelector('dialog');dialog.showModal();
Live Region Politeness, Atomic & Relevant
Fine-tuning what a screen reader announces when dynamic content changes.
<!-- polite: waits for the user to pause; assertive: interrupts immediately --><div aria-live="polite" aria-atomic="true" aria-relevant="additions text"> <span id="count">3</span> items in cart</div><!-- aria-atomic="true" re-reads the whole region, not just the changed node --><!-- role="status" is shorthand for aria-live="polite" + aria-atomic="true" --><div role="status">Saved.</div><!-- role="alert" is shorthand for aria-live="assertive" + aria-atomic="true" --><div role="alert">Payment failed: card declined.</div><!-- Gotcha: content injected into a region that ISN'T yet in the DOM at page load may not be announced - the live region itself must already exist before you mutate its contents. -->
WCAG 2.2 New Success Criteria
Criteria added in WCAG 2.2 that go beyond the commonly-cited 2.1 baseline.
- 2.4.11 Focus Not Obscured (Minimum) - AA- a focused element must not be entirely hidden by sticky headers, footers, or cookie banners
- 2.4.13 Focus Appearance - AAA- focus indicators must meet a minimum size and contrast against adjacent colors
- 2.5.7 Dragging Movements - AA- any drag-based interaction needs a single-pointer alternative (e.g. tap-to-select instead of drag-to-reorder)
- 2.5.8 Target Size (Minimum) - AA- interactive targets must be at least 24x24 CSS pixels unless spaced or inline in text
- 3.2.6 Consistent Help - A- help mechanisms (chat, contact link, FAQ) must appear in the same relative order across pages
- 3.3.7 Redundant Entry - A- don't force users to re-enter information already supplied earlier in the same process
- 3.3.8 Accessible Authentication (Minimum) - AA- cognitive function tests (e.g. puzzle CAPTCHAs) can't be the only login method without an alternative
Respecting prefers-reduced-motion & forced-colors
Adapting animation and color usage for vestibular disorders and Windows High Contrast Mode.
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }}/* Windows High Contrast / forced-colors mode strips custom colors - use system color keywords so borders/icons stay visible */@media (forced-colors: active) { .card { border: 1px solid CanvasText; forced-color-adjust: none; } .icon-only-button { outline: 1px solid ButtonText; }}
Automated tools like axe or Lighthouse only catch roughly a third of WCAG issues: always pair them with a manual keyboard-only pass and a quick screen reader smoke test using VoiceOver or NVDA before calling a page accessible.