Concept
Automated accessibility tools (Lighthouse, axe, WAVE) catch roughly 30-40% of real WCAG issues, missing alt text, color contrast, missing form labels. The rest requires understanding the actual standard and testing with real assistive technology. This topic covers the parts that don't show up in an automated audit.
WCAG conformance levels
WCAG (Web Content Accessibility Guidelines) defines success criteria at three levels:
- A, minimum. Failing A-level criteria makes content unusable for some users entirely (e.g. no text alternative for images).
- AA, the level almost every legal requirement (ADA in the US via case law, EN 301 549 in the EU, AODA in Ontario) references. This is the practical target for any public-facing product.
- AAA, the highest level. Not required or even achievable for all content types (WCAG itself states AAA shouldn't be a blanket site-wide requirement), used selectively for specific criteria where feasible.
The four organizing principles (POUR): content must be Perceivable, Operable, Understandable, and Robust. Every specific success criterion falls under one of these. When evaluating a design or feature, POUR is a useful lens even before checking specific criteria: can everyone perceive this? Operate it? Understand it? Will it keep working across assistive tech?
Keyboard navigation: the real test
If you can't complete every user flow using only Tab, Shift+Tab, Enter, Space, and arrow keys, no mouse, ever, the interface has an accessibility bug, full stop. This is also, not coincidentally, the fastest and cheapest accessibility test you can run without any special tooling.
Focus order should match visual/reading order. CSS that visually reorders content (flex-direction: row-reverse, order, absolute positioning) without a corresponding DOM reorder creates a mismatch, sighted keyboard users see focus jump to visually unexpected places.
tabindex values and what they actually mean:
<div tabindex="0"> <!-- adds to natural tab order, focusable via click too -->
<div tabindex="-1"> <!-- programmatically focusable (el.focus()), NOT in tab order -->
<div tabindex="3"> <!-- DON'T. Positive values override natural DOM order -->Positive tabindex values are almost always a mistake, they create a separate, hard-to-maintain tab sequence disconnected from DOM/visual order, and mixing them with tabindex="0" elements produces genuinely confusing navigation. The correct fix for "this needs to be reachable earlier" is almost always to move the element in the DOM, not to assign it a positive tabindex.
Focus trapping (modals, dialogs)
When a modal opens, keyboard focus must be trapped inside it, Tab from the last focusable element cycles back to the first, and background content isn't reachable at all until the modal closes.
function trapFocus(modalEl) {
const focusable = modalEl.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex="0"]'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modalEl.addEventListener("keydown", (e) => {
if (e.key !== "Tab") return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
}
The native <dialog> element with .showModal() handles focus trapping, Escape-to-close, and background inertness automatically, prefer it over hand-rolled modals when it fits, since it's one less accessibility surface to get wrong. Combine with the inert attribute on background content for non-<dialog> overlay patterns.
Focus restoration
Equally important and equally often forgotten: when a modal, menu, or overlay closes, focus must return to the element that opened it, not fall back to <body> (which resets a screen reader user's position to the very top of the page, disorienting).
let previouslyFocused;
function openModal() {
previouslyFocused = document.activeElement;
modal.showModal();
modal.querySelector("button, input")?.focus();
}