How it works
The explanation, with examples you can run.
Semantic HTML does most of the work
Use the element that means what you want: <button> for actions, <a href> for navigation, <h1> to <h6> in order for structure, <nav>, <main> and <footer> as landmarks that screen reader users jump between, <label for> tied to every input, and <table> with <th> for tabular data. Native controls bring their keyboard behaviour, focusability and role with them.
ARIA is for the gaps: widgets HTML has no element for (tabs, a combobox), or state that changes (aria-expanded, aria-live for status messages). The first rule of ARIA is not to use it when a native element will do, because role="button" only changes the announcement; you still have to add tabindex="0" and the key handling yourself. The ARIA Authoring Practices Guide shows the expected keyboard pattern for each widget.
Names, images and forms
Every interactive element needs an accessible name. A button's text is its name; an icon-only button needs aria-label or visually hidden text. Informative images need alt text that says what the image tells you; decorative ones get alt="" so they are skipped. Errors in forms should be in text, linked to the field with aria-describedby, not only shown in red.
Focus
Tab order follows the DOM order, so keep the DOM in reading order and avoid positive tabindex values. Never remove the focus outline without replacing it; :focus-visible lets you show it for keyboard users only. When a dialog opens, move focus into it and return it to the trigger when it closes (the <dialog> element's showModal() handles much of this).
Contrast and WCAG levels
WCAG success criteria are graded A (the minimum), AA and AAA. AA is what most laws and policies reference, and WCAG 2.2, first published as a W3C Recommendation in October 2023, is the current version. It added nine criteria, including visible focus not hidden by sticky content, minimum target sizes and logins that don't depend on memorising or solving something. It also made 4.1.1 Parsing obsolete.
Contrast is computed from the relative luminance of the two colours. This is the formula from the WCAG 2.2 definitions:
Press Run to see what your code prints, in order.
Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.
Automated checkers catch only part of the real problems. Also try the page with only a keyboard, at 200% zoom, and with a screen reader (VoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android).
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.