Accessibility is not a feature you add after the design is done. It's a constraint you design within from the start. WCAG 2.2 AA is the baseline — not the aspirational target. Semantic HTML, keyboard navigation, colour contrast, and screen reader compatibility are table stakes, not bonus points.
WCAG 2.2 AA defines the success criteria. The headline numbers: 4.5:1 contrast ratio for normal text, 3:1 for large text and UI components. All functionality reachable via keyboard. No information conveyed by colour alone. Focus indicators visible. Error messages associated with their fields. These aren't edge cases — they affect anyone using the interface in bright sunlight, on a small screen, with one hand, or while distracted.
Semantic HTML is free accessibility. A <button> is focusable, clickable, and announced as a button by screen readers — with zero extra code. A <div onclick> does none of that without manual ARIA attributes, keyboard handlers, and role assignments. Start with the right element and you skip half the work.
Role — what is this thing? (button, dialog, navigation, alert). Name — what is it called? (from visible text, aria-label, or aria-labelledby). State — what is it doing right now? (expanded, selected, disabled, checked). If any of these three is missing, the screen reader can't convey the element to the user.
<!-- Skip link — lets keyboard users jump past the nav --> <a href="#main" class="skip-link">Skip to main content</a> <style> .skip-link { position: absolute; top: -40px; left: 0; background: var(--blue); color: white; padding: 8px 16px; z-index: 200; transition: top 0.2s; } .skip-link:focus { top: 0; } </style>
<!-- Modal dialog with focus trap and ARIA --> <div role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc" > <h2 id="dialog-title">Delete account</h2> <p id="dialog-desc">This will permanently remove your data.</p> <button autofocus>Cancel</button> <button>Delete</button> </div> /* Focus trap — keep Tab cycling inside the modal */ function trapFocus(dialog) { const focusable = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const first = focusable[0]; const last = focusable[focusable.length - 1]; dialog.addEventListener('keydown', (e) => { 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(); } }); }
/* Respect reduced motion preference */ @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; } } /* Visible focus indicator — don't remove outline, style it */ :focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; border-radius: 2px; }
| Contrast requirement | Ratio | Applies to |
|---|---|---|
| Normal text (under 18px / 14px bold) | 4.5:1 | Body copy, labels, links |
| Large text (18px+ / 14px+ bold) | 3:1 | Headings, large buttons |
| UI components and graphics | 3:1 | Icons, borders, focus rings |
| AAA enhanced (optional) | 7:1 | Maximum readability |
*:focus { outline: none; } is the single most common accessibility violation on the web. Style the outline instead. Use :focus-visible to only show it for keyboard users.
The first rule of ARIA: don't use ARIA if a native HTML element does the job. <button> beats <div role="button"> every time. ARIA is a polyfill for semantics that HTML doesn't cover — dialogs, tabs, tree views — not a replacement for using the right element.
axe, Lighthouse, and WAVE catch about 30% of accessibility issues. They can't test whether a screen reader announces things in a sensible order, whether the tab sequence is logical, or whether the content makes sense without visual context. Manual testing with VoiceOver, NVDA, or TalkBack is required.
A red border on an error field means nothing to a colourblind user. Add an icon, a text label, or an aria-describedby error message. Never rely on colour alone to convey state.