Skip to content

Accessibility

WCAG 2.2 AA checklist: every A and AA success criterion, with code fixes

Every WCAG 2.2 A and AA success criterion with a failing and a fixed example, and whether an axe-core rule, one of our rules or only a person can catch it.

Your UX Agent teamProduct
75 min read

Last updated

WCAG 2.2 Level AA has 55 success criteria: 31 at Level A and 24 at Level AA. To meet Level AA, a page has to satisfy all 55; there is no partial AA. This checklist goes through every one of them, grouped by WCAG’s four principles. For each criterion you get the rule in plain language, a short example that fails, a fixed version with the reason the fix works, a note on whether an automated tool can catch the problem, and the matching EN 301 549 clause, the European standard that incorporates WCAG and is cited under the Web Accessibility Directive.

Start with the summary table, or jump straight to a criterion: every entry has its own anchor, such as #c-2-5-8 for Target Size (Minimum).

The checklist at a glance

“Detection” names the layers that can catch at least some failures of a criterion: an axe-core rule, our rule (a rule in Your UX Agent’s own catalogue), or manual review by a person. Every row includes “manual”, because automated checks catch some failures of a criterion, not all of them. The EN 301 549 column gives the clause number and the versions it appears in: “both” means V3.2.1 and V4.1.1.

NumberCriterionLevelNew in 2.2DetectionEN 301 549
1.1.1Non-text ContentANoaxe-core, our rule, manual9.1.1.1 · both
1.2.1Audio-only and Video-only (Prerecorded)ANomanual only9.1.2.1 · both
1.2.2Captions (Prerecorded)ANoaxe-core, manual9.1.2.2 · both
1.2.3Audio Description or Media Alternative (Prerecorded)ANomanual only9.1.2.3 · both
1.2.4Captions (Live)AANomanual only9.1.2.4 · both (in neither version’s Web Accessibility Directive table)
1.2.5Audio Description (Prerecorded)AANomanual only9.1.2.5 · both
1.3.1Info and RelationshipsANoaxe-core, our rule (off by default), manual9.1.3.1 · both
1.3.2Meaningful SequenceANomanual only9.1.3.2 · both
1.3.3Sensory CharacteristicsANomanual only9.1.3.3 · both
1.3.4OrientationAANoaxe-core (experimental), manual9.1.3.4 · both
1.3.5Identify Input PurposeAANoaxe-core, our rule, manual9.1.3.5 · both
1.4.1Use of ColorANoaxe-core, our rule, manual9.1.4.1 · both
1.4.2Audio ControlANoaxe-core, our rule, manual9.1.4.2 · both
1.4.3Contrast (Minimum)AANoaxe-core, our rule, manual9.1.4.3 · both
1.4.4Resize TextAANoaxe-core, manual9.1.4.4 · both
1.4.5Images of TextAANomanual only9.1.4.5 · both
1.4.10ReflowAANoour rule, manual9.1.4.10 · both
1.4.11Non-text ContrastAANoour rule, manual9.1.4.11 · both
1.4.12Text SpacingAANoaxe-core, our rule, manual9.1.4.12 · both
1.4.13Content on Hover or FocusAANoour rule, manual9.1.4.13 · both
Perceivable: 20 criteria, 1.1.1 to 1.4.13.
NumberCriterionLevelNew in 2.2DetectionEN 301 549
2.1.1KeyboardANoaxe-core, our rule, manual9.2.1.1 · both
2.1.2No Keyboard TrapANoour rule, manual9.2.1.2 · both
2.1.4Character Key ShortcutsANomanual only9.2.1.4 · both
2.2.1Timing AdjustableANoaxe-core, manual9.2.2.1 · both
2.2.2Pause, Stop, HideANoaxe-core, manual9.2.2.2 · both
2.3.1Three Flashes or Below ThresholdANomanual only9.2.3.1 · both
2.4.1Bypass BlocksANoaxe-core, our rule, manual9.2.4.1 · both
2.4.2Page TitledANoaxe-core, our rule, manual9.2.4.2 · both
2.4.3Focus OrderANoour rule, manual9.2.4.3 · both
2.4.4Link Purpose (In Context)ANoaxe-core, our rule, manual9.2.4.4 · both
2.4.5Multiple WaysAANomanual only9.2.4.5 · both
2.4.6Headings and LabelsAANomanual only9.2.4.6 · both
2.4.7Focus VisibleAANoour rule, manual9.2.4.7 · both
2.4.11Focus Not Obscured (Minimum)AAYesour rule, manual9.2.4.11 · V4.1.1 only
2.5.1Pointer GesturesANomanual only9.2.5.1 · both
2.5.2Pointer CancellationANomanual only9.2.5.2 · both
2.5.3Label in NameANoaxe-core, manual9.2.5.3 · both
2.5.4Motion ActuationANomanual only9.2.5.4 · both
2.5.7Dragging MovementsAAYesmanual only9.2.5.7 · V4.1.1 only
2.5.8Target Size (Minimum)AAYesaxe-core (off by default), our rule, manual9.2.5.8 · V4.1.1 only
Operable: 20 criteria, 2.1.1 to 2.5.8.
NumberCriterionLevelNew in 2.2DetectionEN 301 549
3.1.1Language of PageANoaxe-core, manual9.3.1.1 · both
3.1.2Language of PartsAANoaxe-core, manual9.3.1.2 · both
3.2.1On FocusANoour rule, manual9.3.2.1 · both
3.2.2On InputANomanual only9.3.2.2 · both
3.2.3Consistent NavigationAANoour rule, manual9.3.2.3 · both
3.2.4Consistent IdentificationAANoour rule, manual9.3.2.4 · both
3.2.6Consistent HelpAYesmanual only9.3.2.6 · V4.1.1 only
3.3.1Error IdentificationANomanual only9.3.3.1 · both
3.3.2Labels or InstructionsANoaxe-core, manual9.3.3.2 · both
3.3.3Error SuggestionAANomanual only9.3.3.3 · both
3.3.4Error Prevention (Legal, Financial, Data)AANomanual only9.3.3.4 · both
3.3.7Redundant EntryAYesmanual only9.3.3.7 · V4.1.1 only
3.3.8Accessible Authentication (Minimum)AAYesour rule, manual9.3.3.8 · V4.1.1 only
Understandable: 13 criteria, 3.1.1 to 3.3.8.
NumberCriterionLevelNew in 2.2DetectionEN 301 549
4.1.2Name, Role, ValueANoaxe-core, our rule, manual9.4.1.2 · both
4.1.3Status MessagesAANoour rule (off by default), manual9.4.1.3 · both
Robust: 2 criteria, 4.1.2 and 4.1.3.

Why 55, and where 4.1.1 Parsing went

WCAG 2.2 has 86 success criteria: 31 at Level A, 24 at Level AA and 31 at Level AAA. Level AA conformance means meeting every A and every AA criterion, on whole pages and across complete processes, so the AA checklist has 31 + 24 = 55 entries.

You will see 56 in some checklists. The arithmetic behind that number is WCAG 2.1’s 50 A and AA criteria plus the six new A and AA criteria in 2.2. What it misses is that 2.2 also removed one: 4.1.1 Parsing. So the correct sum is 50 − 1 + 6 = 55. We go through the nine criteria WCAG 2.2 added, including the three at AAA, in a separate post.

WCAG 2.2 still lists 4.1.1 by number, titled “Parsing (Obsolete and removed)”, with no level. W3C’s note explains why: the criterion was there because assistive technology used to parse HTML directly, and “Assistive technology no longer has any need to directly parse HTML.” The problems it guarded against “either no longer exist or are addressed by other criteria”. A duplicate id can still break something, but the failure you would record is the one it causes, under 1.3.1 Info and Relationships or 4.1.2 Name, Role, Value.

EN 301 549 follows the same path. V4.1.1, the version that adopts WCAG 2.2, keeps clause 9.4.1.1 as “Void” with a note pointing to W3C’s reason, so its numbering still matches WCAG’s. V3.2.1, the version cited today under the Web Accessibility Directive, still has 9.4.1.1 Parsing. If your site has to meet V3.2.1, or a contract or policy names WCAG 2.0 or 2.1, keep testing 4.1.1 as well. WCAG 2.1 still lists it at Level A, with a note that it “should be considered as always satisfied for any content using HTML or XML”.

The new criteria also explain why this list is not “2.1 plus a bit”: WCAG 2.2 added nine success criteria in all. Six are at Level A or AA and are in this checklist, marked “New in 2.2”: 2.4.11, 2.5.7, 2.5.8, 3.2.6, 3.3.7 and 3.3.8. The other three (2.4.12, 2.4.13 and 3.3.9) are Level AAA and are not part of AA conformance.

How to use this checklist

The list is ordered the way WCAG is, but that is rarely the best order to work in. If the principles, guidelines and levels are new to you, start with what WCAG is and how its levels work. A practical sequence:

  1. Choose the pages. Most sites are built from a handful of templates: home, listing, detail, article, form, checkout, account. Test one representative page of each, plus every step of each critical process, such as sign-up, checkout or booking. Several criteria (3.2.3, 3.2.4, 3.2.6, 3.3.7) can only be judged across pages or across the steps of a process, so keep the set together.
  2. Run the automated checks first. They are fast and they find the high-volume problems: missing names, low contrast, broken structure. The detection notes below tell you which rule catches what. Fix those before spending a person’s time.
  3. Walk the manual criteria. Use a keyboard only, then zoom to 400%, then a screen reader on the critical processes. The rows marked “manual only” in the table are where no enabled axe-core 4.14.0 rule and none of the rules we run has a check; many others need a person to confirm what a rule could only flag.
  4. Fix by component, not by page. A failing button in your design system fails on every page that uses it. The fixed snippets below are written to be dropped into a shared component.
  5. Re-test what you fixed, with the same check that found it, and keep the results. A dated record of what was tested, how, and what was fixed is the evidence an accessibility statement or an EN 301 549 assessment is built on.

A clean automated run is not a pass. Treat “no findings” as “nothing found by the checks that ran”, and record the criteria you checked by hand separately.

What the detection notes mean

Each entry ends with Can automated tools catch this? and names the layer:

  • axe-core rule: a rule from axe-core, the open-source accessibility testing engine maintained by Deque. We cite version 4.14.0 and only rules that axe-core tags with that criterion at Level A or AA. Rule ids are given so you can look them up in axe-core’s rule list. Some rules can only mark a case for a person to review and never report a failure; the notes say which.
  • Our rule: a rule in Your UX Agent’s own catalogue, mapped to that criterion.
  • Manual review: a person has to judge it, with a keyboard, a screen reader, zoom, or simply by reading.
  • Manual only: no enabled axe-core 4.14.0 rule and no rule we run checks it, so a person has to judge it.

How axe results reach your report: by default our audits run axe-core’s WCAG A and AA rules alongside our own, on the page itself rather than inside the frames it embeds, at the narrowest and widest screen sizes you audit. A failure axe rates critical or serious becomes a finding that names the axe rule it came from, and so does meta-viewport, which axe rates moderate; lesser failures stay as evidence. Where one of our rules checks the same thing, we leave it to ours: always for label and document-title, for color-contrast on a page where ours reported contrast, and for image-alt on the images ours reported. A case axe only marks for review never becomes a finding.

We don’t list model review as a detection layer. A language model can point at something worth checking, but its judgement is not a measurement, so we don’t count it as catching a failure of any criterion.

Three axe-core details matter when you read the notes. First, some rules are off in axe-core’s default configuration: target-size (2.5.8) is the one that matters for AA. It runs only when a tool turns it on: by naming it, or by selecting a tag it carries, such as wcag22aa, wcag258 or, since axe-core 4.14.0, EN-301-549v4. Our AA audits select wcag22aa, so they run it. Second, rules tagged experimental, such as css-orientation-lock (1.3.4), run only when a tool names them or asks for the experimental tag. Our AA audits turn on css-orientation-lock by name; axe’s other experimental rules, such as p-as-heading (1.3.1), do not run in them. Third, label-content-name-mismatch (2.5.3) was experimental until axe-core 4.14.0, which runs it by default; our audits turned it on by name before then. Check what your tool runs before you rely on one.

Of the six A and AA criteria new in 2.2, our own rules cover three: 2.4.11 Focus Not Obscured (Minimum), 2.5.8 Target Size (Minimum) and 3.3.8 Accessible Authentication (Minimum). We have no automated check for 2.5.7 Dragging Movements, 3.2.6 Consistent Help or 3.3.7 Redundant Entry, and axe-core 4.14.0 has no rule for them either.

A rule that finds nothing does not mean the criterion passes. Automated checks measure what they can measure; for the rest, the honest status is “not tested” until a person has looked.

Where EN 301 549 fits

EN 301 549 is the European standard of accessibility requirements for ICT. Its clause 9 holds the requirements for web pages, and they are WCAG success criteria renumbered with a leading 9: WCAG 1.4.3 is clause 9.1.4.3. Two versions matter today:

  • V3.2.1 (2021) adopts WCAG 2.1 Level A and AA. It is the harmonised standard cited under the Web Accessibility Directive, which covers public-sector websites.
  • V4.1.1 adopts WCAG 2.2 Level A and AA in clauses 9, 10 and 11. ETSI published it on 2 September 2026. It is not yet cited in the Official Journal under either the Web Accessibility Directive or the European Accessibility Act. ETSI’s work programme gives 16 December 2026 as its target for the citation, which is a target, not a commitment: the European Commission decides when the reference is published.

No harmonised standard is cited under the European Accessibility Act yet. For what V4.1.1 changes, row by row, and what citation means, see what V4.1.1 changes for websites.

What the version difference means for this checklist:

  • The six criteria new in WCAG 2.2 exist only in V4.1.1 (marked “V4.1.1 only”).
  • 9.4.1.1 is Parsing in V3.2.1 and “Void” in V4.1.1.
  • 9.1.2.4 (live captions) is a clause in both versions, but neither lists it in its Web Accessibility Directive table (V3.2.1’s Table A.1, V4.1.1’s Table ZA.1), because that Directive “does not apply to live time-based media”. V4.1.1 does list it in Table A.1, its web-page table for the European Accessibility Act.
  • V4.1.1 retitles 9.1.2.2 and 9.1.2.4 “Subtitles (pre-recorded)” and “Subtitles (live)”. They still point to WCAG 1.2.2 and 1.2.4, the captions criteria.

The WCAG criteria are not all of clause 9: 9.6 on WCAG conformance, and V4.1.1’s new 9.7 on user preferences, sit beside them, and clauses outside 9 can apply to a website too: see the EN 301 549 clauses that can apply to a website. Meeting the standard in full also takes testing with assistive technologies, which no automated check replaces.

Perceivable

Information and interface components have to reach people through a sense they can use: text alternatives for images, captions and descriptions for media, structure that survives without the visual layout, and enough contrast and flexibility to read.

1.1.1 Non-text Content

Level A · EN 301 549 9.1.1.1 (V3.2.1 and V4.1.1)

Rule. Every image, icon, chart or other non-text content that carries meaning has a text alternative that does the same job. Decorative images are hidden from assistive technology. Controls and inputs need a name that describes their purpose (4.1.2 adds more requirements). Time-based media, tests, sensory experiences and CAPTCHAs need at least a text alternative that identifies them, and a CAPTCHA also needs an alternative form that uses a different sense.

Fails: 1.1.1 Non-text Content, HTML
<img src="/img/delivery-map.png"><img src="/img/wave-divider.svg">
Passes: 1.1.1 Non-text Content, HTML
<img src="/img/delivery-map.png"     alt="Delivery zones: next day in Zone 1 (city centre), two days in Zone 2, three to five days elsewhere"><img src="/img/wave-divider.svg" alt="">

Why it works. The first alt says what the map tells a sighted visitor, not what it looks like. The empty alt="" marks the divider as decoration, so a screen reader skips it instead of reading out the file name.

Can automated tools catch this? Partly. axe-core’s image-alt, input-image-alt, object-alt, role-img-alt and svg-img-alt (plus aria-meter-name and aria-progressbar-name) flag non-text content with no text alternative at all. Our rule image-alt reports images rendered with no alt attribute. No rule can tell whether an alternative is accurate, whether an image marked decorative actually carries information, or whether a chart needs a longer description: that needs manual review.

1.2.1 Audio-only and Video-only (Prerecorded)

Level A · EN 301 549 9.1.2.1 (V3.2.1 and V4.1.1)

Rule. Recorded audio with no video, such as a podcast, needs a text transcript. Recorded video with no audio, such as a silent product animation, needs either a text alternative or an audio track that describes it. The exception is media that is itself an alternative to text on the page and is labelled as such.

Fails: 1.2.1 Audio-only and Video-only (Prerecorded), HTML
<h2>Episode 14: Planning a site migration</h2><audio controls src="/media/episode-14.mp3"></audio>
Passes: 1.2.1 Audio-only and Video-only (Prerecorded), HTML
<h2>Episode 14: Planning a site migration</h2><audio controls src="/media/episode-14.mp3"></audio><p><a href="/podcast/episode-14/transcript">Read the transcript of episode 14</a></p>

Why it works. A transcript gives people who cannot hear the audio the same content, and it is searchable for everyone. It has to cover everything said, with speakers identified, not a summary.

Can automated tools catch this? No. This is manual only: axe-core 4.14.0 has no enabled rule for it (its audio-caption rule is deprecated and off by default). A tool can see an <audio> element, but not whether a transcript exists elsewhere or is complete.

1.2.2 Captions (Prerecorded)

Level A · EN 301 549 9.1.2.2 (V3.2.1 “Captions”, V4.1.1 “Subtitles”)

Rule. Recorded video with sound has synchronised captions for all speech and for sounds that matter, unless the video is a labelled alternative to text already on the page.

Fails: 1.2.2 Captions (Prerecorded), HTML
<video controls src="/media/product-tour.mp4"></video>
Passes: 1.2.2 Captions (Prerecorded), HTML
<video controls>  <source src="/media/product-tour.mp4" type="video/mp4">  <track kind="captions" src="/media/product-tour.en.vtt"         srclang="en" label="English" default></video>
Passes: 1.2.2 Captions (Prerecorded), WebVTT
WEBVTT​00:00:01.000 --> 00:00:04.000[upbeat music]​00:00:04.500 --> 00:00:08.000Welcome. In two minutes you will set up your first project.

Why it works. The <track> gives the player a caption file it can show in time with the video, and the cues include the music as well as the speech, which is what separates captions from subtitles that carry only the dialogue.

Can automated tools catch this? Partly. axe-core’s video-caption reports a <video> with no captions track as needing review; it never reports a failure, because it cannot tell whether captions are burned in. Whether the captions are accurate, complete and in sync, and captions handled inside an embedded player, need a person to check. Watch with the sound off.

1.2.3 Audio Description or Media Alternative (Prerecorded)

Level A · EN 301 549 9.1.2.3 (V3.2.1 and V4.1.1)

Rule. When recorded video shows information that the soundtrack does not say, provide either audio description or a full text alternative that covers both what is said and what is shown.

Fails: 1.2.3 Audio Description or Media Alternative (Prerecorded), HTML
<video controls>  <source src="/media/setup-demo.mp4" type="video/mp4">  <track kind="captions" src="/media/setup-demo.en.vtt" srclang="en" label="English"></video><!-- The narrator says "click here, then here"; the steps appear only on screen. -->
Passes: 1.2.3 Audio Description or Media Alternative (Prerecorded), HTML
<video controls>  <source src="/media/setup-demo.mp4" type="video/mp4">  <track kind="captions" src="/media/setup-demo.en.vtt" srclang="en" label="English"></video><p>  <a href="/help/setup-demo-text">Read the setup demo as text, step by step</a></p>

Why it works. At Level A, a full text alternative, covering what is said and every step shown on screen, is enough. Level AA (1.2.5) raises the bar to audio description.

Can automated tools catch this? No. This is manual only: deciding whether the video shows something the soundtrack does not say takes a person watching it.

1.2.4 Captions (Live)

Level AA · EN 301 549 9.1.2.4 (V3.2.1 “Captions (live)”, V4.1.1 “Subtitles (live)”; outside both versions’ Web Accessibility Directive tables)

Rule. Live video with sound, such as a webinar or a live stream, has real-time captions.

Fails: 1.2.4 Captions (Live), HTML
<h2>Live now: product update webinar</h2><iframe src="https://stream.example.com/embed/product-update"        title="Live stream: product update webinar"></iframe>
Passes: 1.2.4 Captions (Live), HTML
<h2>Live now: product update webinar</h2><iframe src="https://stream.example.com/embed/product-update"        title="Live stream: product update webinar"></iframe><p>  Live captions are written by a captioner during the stream.  <a href="https://captions.example.com/product-update">Open the live captions</a>  in a separate window, or turn on CC in the player.</p>

Why it works. Live captions have to be produced as the event happens, by a human captioner or a captioning service, and shown with the stream or beside it. Plan them when you book the event, not on the day.

Can automated tools catch this? No. This is manual only: no tool can tell from the page whether a live stream will be captioned.

1.2.5 Audio Description (Prerecorded)

Level AA · EN 301 549 9.1.2.5 (V3.2.1 and V4.1.1)

Rule. Recorded video has audio description of the visual information the soundtrack leaves out. At this level a text alternative alone is no longer enough. A video whose narration already says everything that matters needs nothing extra.

Fails (narration script):

Fails: 1.2.5 Audio Description (Prerecorded), text
[On screen: the monthly price drops from €49 to €29]Narrator: "And look at this!"

Passes (the same scene, with the description written into the narration):

Passes: 1.2.5 Audio Description (Prerecorded), text
[On screen: the monthly price drops from €49 to €29]Narrator: "The monthly price drops from 49 to 29 euros."

Why it works. Writing the visual information into the main narration, sometimes called integrated description, means one version of the video serves everyone. For existing videos, publish a described version alongside the original.

Can automated tools catch this? No. This is manual only: it takes a person to compare what is shown with what is said.

1.3.1 Info and Relationships

Level A · EN 301 549 9.1.3.1 (V3.2.1 and V4.1.1)

Rule. Structure that is visible on screen, such as headings, lists, tables, groups of fields and labels, is also in the markup, so assistive technology can convey it.

Fails: 1.3.1 Info and Relationships, HTML
<div class="title-lg">Shipping options</div><div class="row"><b>Standard</b> 3–5 days, free</div><div class="row"><b>Express</b> next day, €9</div>
Passes: 1.3.1 Info and Relationships, HTML
<h2>Shipping options</h2><table>  <caption>Delivery times and prices</caption>  <thead>    <tr>      <th scope="col">Option</th>      <th scope="col">Delivery</th>      <th scope="col">Price</th>    </tr>  </thead>  <tbody>    <tr><th scope="row">Standard</th><td>3–5 days</td><td>Free</td></tr>    <tr><th scope="row">Express</th><td>Next day</td><td>€9</td></tr>  </tbody></table>

Why it works. A screen reader now announces a heading, then a table with three columns, and reads each price with its row and column headers. With the <div> version it reads a run of text with no structure.

Can automated tools catch this? Partly. axe-core’s list, listitem, definition-list, dlitem, td-headers-attr, aria-required-children, aria-required-parent and aria-hidden-body catch structure that is marked up but broken, and th-has-data-cells marks a header cell with no data cells for review, never as a failure. They cannot see that a styled <div> should be a heading or that a grid of <div>s is really a table; axe-core’s experimental p-as-heading attempts the heading case, but our audits do not turn it on. Our rule form-error-not-identified reaches one narrow case: a new or changed error on an invalid field without aria-invalid="true", without aria-describedby or aria-errormessage pointing at that message, and without a changed live region carrying it, so the message is tied to its field only by where it sits on screen. A message that repeats the field’s label as a whole word, not its placeholder, such as “Email is required” under a field labelled Email, ties it in text, so the check skips it. Unchanged hints do not count. Unlinked messages are inferred within the form. The check empties or mistypes fields, never submits, and skips sign-in and payment forms; it is off unless you switch it on. What it reports counts against this criterion, and finding nothing says nothing about the rest of it. Comparing the visual structure with the markup needs manual review.

1.3.2 Meaningful Sequence

Level A · EN 301 549 9.1.3.2 (V3.2.1 and V4.1.1)

Rule. When the order of content affects its meaning, the order in the code matches the order a sighted reader follows.

Fails: 1.3.2 Meaningful Sequence, HTML
<ol class="steps">  <li class="step-3">Confirm your email address</li>  <li class="step-1">Create an account</li>  <li class="step-2">Choose a plan</li></ol>
Fails: 1.3.2 Meaningful Sequence, CSS
.steps { display: flex; flex-direction: column; }.step-1 { order: 1; }.step-2 { order: 2; }.step-3 { order: 3; }
Passes: 1.3.2 Meaningful Sequence, HTML
<ol class="steps">  <li>Create an account</li>  <li>Choose a plan</li>  <li>Confirm your email address</li></ol>

Why it works. CSS order changes only what is painted. Screen readers, reader modes and keyboard users follow the source order, so the source has to carry the sequence.

Can automated tools catch this? No. This is manual only: a tool can find order or flex-direction: row-reverse in CSS, but only a person can say whether the changed order changes the meaning. Read the page with styles switched off, or with a screen reader.

1.3.3 Sensory Characteristics

Level A · EN 301 549 9.1.3.3 (V3.2.1 and V4.1.1)

Rule. Instructions do not rely only on shape, colour, size, position, orientation or sound. They can mention those, as long as something else identifies the thing too.

Fails: 1.3.3 Sensory Characteristics, HTML
<p>To continue, press the green button on the right.</p>
Passes: 1.3.3 Sensory Characteristics, HTML
<p>  To continue, select <strong>Go to payment</strong>,  the green button below your order summary.</p>

Why it works. The button’s name identifies it for someone who cannot see the colour or the layout, and on a phone, where “on the right” may have become “below”.

Can automated tools catch this? No. This is manual only: it is a question of what your instructions say.

1.3.4 Orientation

Level AA · EN 301 549 9.1.3.4 (V3.2.1 and V4.1.1)

Rule. Content works in both portrait and landscape, unless one orientation is essential, as it can be for a piano app or a bank cheque scan.

Fails: 1.3.4 Orientation, CSS
@media (orientation: portrait) {  .dashboard {    transform: rotate(90deg);    transform-origin: top left;  }}
Passes: 1.3.4 Orientation, CSS
.dashboard {  display: grid;  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));  gap: 1rem;}

Why it works. Rotating the layout forces a landscape view on someone whose device may be mounted in portrait, on a wheelchair for example. A layout that reflows to the space it has works either way.

Can automated tools catch this? Partly. axe-core’s css-orientation-lock looks for CSS that locks the view to one orientation, but it is experimental and runs only when a tool switches it on; our AA audits do. A “please rotate your device” screen or a script that locks orientation needs manual review: turn the device.

1.3.5 Identify Input Purpose

Level AA · EN 301 549 9.1.3.5 (V3.2.1 and V4.1.1)

Rule. Fields that ask for information about the user, such as their name, email, address or phone number, declare their purpose in code, using the autocomplete values WCAG lists.

Fails: 1.3.5 Identify Input Purpose, HTML
<label for="n">Name</label><input id="n" name="n"><label for="e">Email</label><input id="e" name="e" autocomplete="mail">
Passes: 1.3.5 Identify Input Purpose, HTML
<label for="name">Full name</label><input id="name" name="name" autocomplete="name"><label for="email">Email</label><input id="email" name="email" type="email" autocomplete="email"><label for="phone">Phone</label><input id="phone" name="phone" type="tel" autocomplete="tel">

Why it works. Browsers and password managers can fill the fields, which helps people with memory or motor impairments, and some assistive tools can show a familiar icon next to each one. mail is not a valid token; email is.

Can automated tools catch this? Partly. axe-core’s autocomplete-valid flags an autocomplete value that is not valid for the field, but not a field with no autocomplete at all. Our rule input-type-mismatch reports fields that do not declare the type, inputmode or autocomplete value matching what they ask for. Which fields collect information about the user is a judgement that needs manual review.

1.4.1 Use of Color

Level A · EN 301 549 9.1.4.1 (V3.2.1 and V4.1.1)

Rule. Colour is never the only way to show information, mark an action or tell elements apart. Links in running text, required fields, errors and chart series each need a second cue.

Fails: 1.4.1 Use of Color, HTML
<p class="body">  Read our <a class="quiet" href="/returns">returns policy</a>  before you send anything back.</p>
Fails: 1.4.1 Use of Color, CSS
.body { color: #333333; }.quiet { color: #2b5797; text-decoration: none; }
Passes: 1.4.1 Use of Color, HTML
<p class="body">  Read our <a href="/returns">returns policy</a>  before you send anything back.</p>
Passes: 1.4.1 Use of Color, CSS
.body { color: #333333; }.body a { color: #2b5797; text-decoration: underline; text-underline-offset: 0.15em; }

Why it works. The underline marks the link for people who cannot tell the two colours apart. W3C also accepts a link colour with at least 3:1 contrast against the surrounding text plus a non-colour cue on hover and focus (technique G183), but the underline is simpler and holds up better.

Can automated tools catch this? Partly. axe-core’s link-in-text-block flags a link in a block of text that only its colour sets apart, with no underline or other style, when its text colour has under 3:1 contrast with the surrounding text and its background has under 3:1 with the surrounding background. Our rule link-affordance reports links in body text with no underline and a colour under 3:1 against the text around them. Neither checks the cue on hover and focus that G183 adds to a 3:1 link colour. Charts, status colours and red-only error states need manual review.

1.4.2 Audio Control

Level A · EN 301 549 9.1.4.2 (V3.2.1 and V4.1.1)

Rule. If audio plays automatically for more than three seconds, there is a way to pause or stop it, or to set its volume separately from the system volume.

Fails: 1.4.2 Audio Control, HTML
<video autoplay loop src="/media/brand-film.mp4"></video>
Passes: 1.4.2 Audio Control, HTML
<video autoplay muted loop playsinline controls src="/media/brand-film.mp4"></video>

Why it works. Autoplaying sound talks over a screen reader, so its user cannot hear the page well enough to find the stop button. Muted autoplay with controls lets visitors opt in to the sound.

Can automated tools catch this? Partly. axe-core’s no-autoplay-audio reports <video> and <audio> elements that autoplay sound for over three seconds without controls, as needing review. Our rule autoplay-audio-uncontrolled reports a media element set to autoplay that is not muted, has no player controls of its own, and is not known to stop within three seconds. Sound started by scripts or inside third-party embeds needs manual review: load the page with the sound on.

1.4.3 Contrast (Minimum)

Level AA · EN 301 549 9.1.4.3 (V3.2.1 and V4.1.1)

Rule. Text and images of text have a contrast ratio of at least 4.5:1 against their background. Large text, at least 18 point or 14 point bold, needs 3:1. Text in logos, text that is pure decoration, text on inactive controls, invisible text and text that is an incidental part of a picture are exempt.

Fails: 1.4.3 Contrast (Minimum), CSS
.card-meta {  color: #999999;      /* 2.85:1 on white */  background: #ffffff;}
Passes: 1.4.3 Contrast (Minimum), CSS
.card-meta {  color: #767676;      /* 4.54:1 on white */  background: #ffffff;}

Why it works. #767676 is about the lightest grey that still reaches 4.5:1 on white. Define it once as a design token so every component inherits a passing value.

We go deeper on contrast ratios for text, icons and focus rings, including how to pick a passing colour.

Can automated tools catch this? Partly, and reliably for text on a solid background. axe-core’s color-contrast and our rule contrast measure the computed colours. axe-core reports cases it cannot compute, such as text on images or gradients, as needing review rather than as failures. Hover and focus states on controls a tool does not reach, error states, text in images and text over video need manual review.

1.4.4 Resize Text

Level AA · EN 301 549 9.1.4.4 (V3.2.1 and V4.1.1)

Rule. Text can be resized to 200% without assistive technology and without losing content or functionality. Captions and images of text are exempt.

Fails: 1.4.4 Resize Text, HTML
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
Fails: 1.4.4 Resize Text, CSS
.tab-label { height: 20px; overflow: hidden; font-size: 14px; }
Passes: 1.4.4 Resize Text, HTML
<meta name="viewport" content="width=device-width, initial-scale=1">
Passes: 1.4.4 Resize Text, CSS
.tab-label { min-height: 1.5em; font-size: 0.875rem; }

Why it works. Without user-scalable=no and maximum-scale=1, people can pinch to zoom. Sizes in rem and em follow the user’s text-size setting, and min-height lets a box grow with its text instead of clipping it.

Can automated tools catch this? Partly. axe-core’s meta-viewport flags a viewport tag that disables zoom or caps it too low. Clipped, overlapping or hidden text at 200% needs manual review: zoom the browser to 200% and use the page.

1.4.5 Images of Text

Level AA · EN 301 549 9.1.4.5 (V3.2.1 and V4.1.1)

Rule. Use real text, not pictures of text, unless the image can be customised by the user or a particular presentation is essential, as in a logo or a font sample.

Fails: 1.4.5 Images of Text, HTML
<img src="/img/spring-sale-banner.png" alt="Spring sale: 20% off all jackets until 30 April">
Passes: 1.4.5 Images of Text, HTML
<section class="promo">  <h2>Spring sale</h2>  <p>20% off all jackets until 30 April.</p></section>
Passes: 1.4.5 Images of Text, CSS
.promo {  color: #ffffff;  background:    linear-gradient(rgb(0 0 0 / 0.6), rgb(0 0 0 / 0.6)),    url("/img/spring-bg.jpg") center / cover;}

Why it works. Real text can be resized, recoloured, translated and read in a reader mode. The dark overlay keeps the white text readable over any part of the photo, which also helps with 1.4.3.

Can automated tools catch this? No. This is manual only: a tool cannot reliably tell that an image contains text.

1.4.10 Reflow

Level AA · EN 301 549 9.1.4.10 (V3.2.1 and V4.1.1)

Rule. Content works at a width of 320 CSS pixels, which is a 1280-pixel window zoomed to 400%, without scrolling in two directions and without losing content or functionality. Content that needs a two-dimensional layout, such as maps, diagrams and data tables, is exempt.

Fails: 1.4.10 Reflow, CSS
.layout {  display: grid;  grid-template-columns: 280px 900px;}
Passes: 1.4.10 Reflow, CSS
.layout {  display: grid;  grid-template-columns: minmax(0, 1fr);}@media (min-width: 64em) {  .layout { grid-template-columns: 17.5rem minmax(0, 1fr); }}img, video { max-width: 100%; height: auto; }

Why it works. One flexible column at narrow widths, and a sidebar only when there is room for it. minmax(0, 1fr) lets the column shrink below the width of its widest child instead of pushing the page sideways.

Can automated tools catch this? Partly. Our rules responsive-overflow and offscreen-interactive report a page that is wider than the screen, and controls left beyond its edge, at the widths we test, down to 320 pixels. axe-core 4.14.0 has no rule for this. Content that is cut off or hidden rather than overflowing, and whether a section qualifies for the two-dimensional exception, need manual review.

1.4.11 Non-text Contrast

Level AA · EN 301 549 9.1.4.11 (V3.2.1 and V4.1.1)

Rule. The parts of controls you need to see to use them, such as a field’s border, a checkbox outline, a focus ring or a selected state, and the parts of graphics you need to understand them, have at least 3:1 contrast against the colours next to them. Inactive controls, unstyled browser defaults and graphics whose presentation is essential, such as logos, are exempt.

Fails: 1.4.11 Non-text Contrast, CSS
input[type="text"] {  border: 1px solid #dddddd;   /* 1.36:1 on white */  background: #ffffff;}
Passes: 1.4.11 Non-text Contrast, CSS
input[type="text"] {  border: 1px solid #8a8a8a;   /* 3.45:1 on white */  background: #ffffff;}input[type="text"]:focus-visible {  outline: 2px solid #1a56db;  outline-offset: 2px;}

Why it works. A field whose border is nearly invisible is hard to find for people with low vision. At 3:1 the edge stays visible, and a focus ring in a strong colour, offset from the border, makes the active field obvious.

Can automated tools catch this? Partly. Our rule non-text-contrast measures a control’s border against the surface behind it. axe-core 4.14.0 has no rule for this criterion. Focus indicators, icons, chart lines and segments, and states such as “selected” need manual review.

1.4.12 Text Spacing

Level AA · EN 301 549 9.1.4.12 (V3.2.1 and V4.1.1)

Rule. Nothing is lost when a user sets line height to 1.5 times the font size, paragraph spacing to 2 times, letter spacing to 0.12 times and word spacing to 0.16 times.

Fails: 1.4.12 Text Spacing, CSS
.card-title {  height: 48px;  line-height: 24px;  overflow: hidden;}
Passes: 1.4.12 Text Spacing, CSS
.card-title {  min-height: 3em;  line-height: 1.5;}

To test, apply these overrides, with a bookmarklet or a user stylesheet:

Test overrides: 1.4.12 Text Spacing, CSS
* {  line-height: 1.5 !important;  letter-spacing: 0.12em !important;  word-spacing: 0.16em !important;}p { margin-bottom: 2em !important; }

Why it works. A fixed height with overflow: hidden cuts off the second line as soon as spacing grows. A minimum height in em lets the box grow with its text.

Can automated tools catch this? Partly. axe-core’s avoid-inline-spacing flags letter, word or line spacing that a style attribute sets with !important below the criterion’s values, which the bookmarklets and extensions people use to change text spacing cannot override. Our rule text-spacing-loss applies the overrides and reports content they clip or hide. Overlaps that make text unreadable without hiding it need manual review.

1.4.13 Content on Hover or Focus

Level AA · EN 301 549 9.1.4.13 (V3.2.1 and V4.1.1)

Rule. Content that appears on hover or focus, such as tooltips and hover menus, can be dismissed without moving the pointer or focus (usually with Esc), unless it reports an input error or covers nothing, can be hovered itself without disappearing, and stays until the user moves away, dismisses it, or it stops being relevant. Browser-native tooltips from the title attribute are exempt.

Fails: 1.4.13 Content on Hover or Focus, HTML
<span class="has-tip">VAT</span><span class="tip">Prices include VAT.</span>
Fails: 1.4.13 Content on Hover or Focus, CSS
.tip { display: none; position: absolute; }.has-tip:hover + .tip { display: block; }
Passes: 1.4.13 Content on Hover or Focus, HTML
<span class="tip-wrap">  <button type="button" aria-describedby="vat-tip">VAT</button>  <span role="tooltip" id="vat-tip" class="tip">Prices include VAT.</span></span><script>  document.addEventListener("keydown", (event) => {    if (event.key !== "Escape") return;    document.querySelectorAll(".tip-wrap:hover, .tip-wrap:focus-within")      .forEach((wrap) => wrap.classList.add("dismissed"));  });  document.querySelectorAll(".tip-wrap").forEach((wrap) => {    wrap.addEventListener("mouseleave", () => wrap.classList.remove("dismissed"));    wrap.addEventListener("focusout", () => wrap.classList.remove("dismissed"));  });</script>
Passes: 1.4.13 Content on Hover or Focus, CSS
.tip-wrap { position: relative; }.tip { display: none; position: absolute; top: 100%; left: 0; }.tip-wrap:hover .tip,.tip-wrap:focus-within .tip { display: block; }.tip-wrap.dismissed .tip { display: none; }

Why it works. The tooltip sits inside the element that is hovered, directly below the trigger with no gap, so moving the pointer onto it keeps it open. It also opens on keyboard focus, and Esc hides it without moving anything. Someone using screen magnification can now move over to read it.

Can automated tools catch this? Partly. Our rule hover-content-unreliable points at and focuses up to eight controls a page, those that announce extra content first and then links and buttons, on up to 25 pages an audit, at the widest width audited. It reports content that disappears when the pointer moves onto it, disappears on its own, or ignores Esc while covering other content. axe-core 4.14.0 has no rule for it. Other controls, controls inside a form, other pages and narrower widths need manual review: hover each trigger, move the pointer onto the content, press Esc, and wait to see whether it disappears on its own.

Operable

Everything has to work with whatever input a person uses: a keyboard, a single finger, a switch, voice. People need enough time, nothing that flashes dangerously, and a way to find where they are and where they can go.

2.1.1 Keyboard

Level A · EN 301 549 9.2.1.1 (V3.2.1 and V4.1.1)

Rule. All functionality works from a keyboard, without having to time your key presses. The exception is a function that depends on the path of the movement itself, such as freehand drawing.

Fails: 2.1.1 Keyboard, HTML
<div class="btn" onclick="addToCart()">Add to cart</div>
Passes: 2.1.1 Keyboard, HTML
<button type="button" onclick="addToCart()">Add to cart</button>

Why it works. A <button> is in the tab order and fires its click handler on Enter and Space without any extra code. A <div> needs tabindex, a role and key handling to match it, and usually ends up missing one of them.

Can automated tools catch this? Partly. axe-core’s scrollable-region-focusable and frame-focusable-content catch two specific cases of content a keyboard cannot reach: scrollable regions that cannot take focus, and frames with focusable content set to tabindex="-1". Our audits run axe on the page, not inside its frames, so frame-focusable-content never applies in them. axe-core’s server-side-image-map marks a server-side image map for review, never as a failure. Our rule keyboard-focus reports controls that would not take focus when we focused each one in turn, and positive tabindex values. clickable-not-focusable reports elements with an inline onclick (or data-href/data-action) attribute that are not buttons or links and cannot take focus. Click handlers added in script are not detected. Whether every function actually works by keyboard, such as a custom slider’s arrow keys or a menu that opens only on hover, needs manual review.

2.1.2 No Keyboard Trap

Level A · EN 301 549 9.2.1.2 (V3.2.1 and V4.1.1)

Rule. If keyboard focus can move into a component, it can also move out using the keyboard. If leaving needs more than Tab, Shift+Tab, the arrow keys or another standard way out, the page tells the user how.

Fails: 2.1.2 No Keyboard Trap, HTML
<label for="code">Snippet</label><textarea id="code"></textarea><script>  const editor = document.querySelector("#code");  editor.addEventListener("keydown", (event) => {    if (event.key === "Tab") {      event.preventDefault();      editor.setRangeText("\t", editor.selectionStart, editor.selectionEnd, "end");    }  });</script>
Passes: 2.1.2 No Keyboard Trap, HTML
<label for="code">Snippet</label><p id="code-hint">Tab inserts a tab character. Press Esc, then Tab, to leave the editor.</p><textarea id="code" aria-describedby="code-hint"></textarea><script>  const editor = document.querySelector("#code");  let tabInserts = true;  editor.addEventListener("focus", () => { tabInserts = true; });  editor.addEventListener("keydown", (event) => {    if (event.key === "Escape") { tabInserts = false; return; }    if (event.key === "Tab" && tabInserts) {      event.preventDefault();      editor.setRangeText("\t", editor.selectionStart, editor.selectionEnd, "end");    }  });</script>

Why it works. Esc releases Tab back to the browser, so focus can leave, and the visible hint, also linked to the field, tells people how before they get stuck.

Can automated tools catch this? Partly. Our rule keyboard-trap reports when tabbing cycles back to where it started without reaching most of the page. axe-core 4.14.0 has no rule for this criterion. Traps inside embedded players, iframes and widgets that you enter with the arrow keys need manual review.

2.1.4 Character Key Shortcuts

Level A · EN 301 549 9.2.1.4 (V3.2.1 and V4.1.1)

Rule. A keyboard shortcut that uses a single letter, number, punctuation mark or symbol can be turned off, can be remapped to include a modifier key such as Ctrl or Alt, or works only while the relevant component has focus.

Fails: 2.1.4 Character Key Shortcuts, JavaScript
document.addEventListener("keydown", (event) => {  if (event.key === "d") deleteSelectedMessage();});
Passes: 2.1.4 Character Key Shortcuts, JavaScript
const messageList = document.querySelector("#messages");messageList.addEventListener("keydown", (event) => {  if (event.key === "d") deleteSelectedMessage();});

Why it works. Speech-input users dictate words, and every “d” in “delete the draft” would fire a page-wide shortcut. Listening on the message list means the shortcut only works while focus is inside that list, which is one of the three ways the criterion allows.

Can automated tools catch this? No. This is manual only: test with a speech input tool, or simply type text on the page outside form fields and watch what happens.

2.2.1 Timing Adjustable

Level A · EN 301 549 9.2.2.1 (V3.2.1 and V4.1.1)

Rule. Every time limit can be turned off, adjusted to at least ten times its length, or extended: the user is warned before time runs out, gets at least 20 seconds to respond with a simple action, and can extend at least ten times. Real-time events, essential limits and limits over 20 hours are exempt.

Fails: 2.2.1 Timing Adjustable, HTML
<meta http-equiv="refresh" content="900;url=/signed-out">
Passes: 2.2.1 Timing Adjustable, HTML
<dialog id="timeout" aria-labelledby="timeout-title">  <h2 id="timeout-title">Still there?</h2>  <p>We will sign you out in 2 minutes to protect your account.</p>  <form method="dialog">    <button value="stay">Stay signed in</button>  </form></dialog><script>  const dialog = document.querySelector("#timeout");  const warnAfter = 13 * 60 * 1000;  let warning = setTimeout(() => dialog.showModal(), warnAfter);  dialog.addEventListener("close", () => {    if (dialog.returnValue !== "stay") return;    fetch("/session/extend", { method: "POST" });    clearTimeout(warning);    warning = setTimeout(() => dialog.showModal(), warnAfter);  });</script>

Why it works. The warning appears two minutes before the session ends, and one button press extends it, as often as needed. People who read, type or move slowly keep their work. The extend request is your own session endpoint.

Can automated tools catch this? Partly. axe-core’s meta-refresh flags a <meta http-equiv="refresh"> that reloads or redirects after a delay of more than zero and up to 20 hours. Session timeouts, timed offers and quizzes set in scripts need manual review.

2.2.2 Pause, Stop, Hide

Level A · EN 301 549 9.2.2.2 (V3.2.1 and V4.1.1)

Rule. Anything that moves, blinks or scrolls on its own for more than five seconds alongside other content can be paused, stopped or hidden. Content that updates on its own, such as a news ticker, can be paused, stopped, hidden or have its update rate changed. Movement that is essential is exempt.

Fails: 2.2.2 Pause, Stop, Hide, HTML
<div class="ticker">  <p>Free delivery on orders over €50 · Gift cards now available</p></div>
Fails: 2.2.2 Pause, Stop, Hide, CSS
.ticker { overflow: hidden; }.ticker p { animation: scroll-left 12s linear infinite; }@keyframes scroll-left {  from { transform: translateX(100%); }  to { transform: translateX(-100%); }}
Passes: 2.2.2 Pause, Stop, Hide, HTML
<div class="ticker">  <button type="button" id="ticker-pause" aria-pressed="false">Pause</button>  <p>Free delivery on orders over €50 · Gift cards now available</p></div><script>  const pause = document.querySelector("#ticker-pause");  pause.addEventListener("click", () => {    const paused = pause.closest(".ticker").classList.toggle("paused");    pause.setAttribute("aria-pressed", String(paused));  });</script>
Passes: 2.2.2 Pause, Stop, Hide, CSS
.ticker { overflow: hidden; }.ticker p { animation: scroll-left 12s linear infinite; }.ticker.paused p { animation-play-state: paused; }@media (prefers-reduced-motion: reduce) {  .ticker p { animation: none; }}@keyframes scroll-left {  from { transform: translateX(100%); }  to { transform: translateX(-100%); }}

Why it works. The button gives everyone a way to stop the movement, which the criterion requires. Honouring prefers-reduced-motion is a good extra, but on its own it does not meet the criterion, because not everyone who is distracted by motion has set it.

Can automated tools catch this? Partly. axe-core’s blink and marquee flag the two obsolete HTML elements. CSS and script animation, carousels and auto-updating feeds need manual review.

2.3.1 Three Flashes or Below Threshold

Level A · EN 301 549 9.2.3.1 (V3.2.1 and V4.1.1)

Rule. Nothing flashes more than three times in any one second, unless the flashing stays below WCAG’s general flash and red flash thresholds.

Fails: 2.3.1 Three Flashes or Below Threshold, CSS
.alert-flash {  min-height: 300px;  animation: flash 0.2s steps(1) infinite;}@keyframes flash { 50% { background: #ff0000; } }
Passes: 2.3.1 Three Flashes or Below Threshold, CSS
.alert-pulse { animation: pulse 2s ease-in-out 3; }@keyframes pulse { 50% { box-shadow: 0 0 0 4px rgb(200 30 30 / 0.4); } }

Why it works. The first rule flashes a large red banner five times a second, over an area big enough to cross the flash threshold, which can trigger seizures. The second pulses a thin outline once every two seconds, three times, then stops.

Can automated tools catch this? Not with axe-core or our rules; this is manual only. Animations can be read from the code; video and animated images need a frame-by-frame flash analysis, for example with the Trace Center’s PEAT, before you publish them.

2.4.1 Bypass Blocks

Level A · EN 301 549 9.2.4.1 (V3.2.1 and V4.1.1)

Rule. There is a way to skip blocks that repeat across pages, such as the header and main navigation.

Fails: 2.4.1 Bypass Blocks, HTML
<div class="header">  <div class="menu"><!-- 40 navigation links --></div></div><div class="content"><!-- page content --></div>
Passes: 2.4.1 Bypass Blocks, HTML
<a class="skip-link" href="#main">Skip to main content</a><header>  <nav aria-label="Main"><!-- 40 navigation links --></nav></header><main id="main" tabindex="-1"><!-- page content --></main>
Passes: 2.4.1 Bypass Blocks, CSS
.skip-link { position: absolute; left: -9999px; }.skip-link:focus { left: 1rem; top: 1rem; }

Why it works. Keyboard users reach “Skip to main content” with the first Tab and jump past forty stops. Screen reader users also get landmarks (<header>, <nav>, <main>) to move between. tabindex="-1" lets the <main> element receive focus when the link is followed.

Can automated tools catch this? Partly. axe-core’s bypass marks a page for review, never as a failure, when it has no heading, no main landmark and no same-page link. Our rule bypass-mechanism-missing reports a page whose header or navigation holds five or more links and that has no heading, no main landmark and no working same-page link among its first tab stops. Whether the skip link appears on focus and really moves focus needs manual review.

2.4.2 Page Titled

Level A · EN 301 549 9.2.4.2 (V3.2.1 and V4.1.1)

Rule. Every page has a title that describes its topic or purpose.

Fails (on the order confirmation page):

Fails: 2.4.2 Page Titled, HTML
<title>Example Shop</title>
Passes: 2.4.2 Page Titled, HTML
<title>Your order is confirmed – Example Shop</title>

In a single-page app, set the title on every view change:

Single-page app: 2.4.2 Page Titled, JavaScript
function showView(view) {  renderView(view);  document.title = `${view.title} – Example Shop`;}

Why it works. The title is the first thing a screen reader announces and the text people see in tabs, bookmarks and history. Putting the page’s own topic first makes it distinguishable among open tabs.

Can automated tools catch this? Partly. axe-core’s document-title and our rule document-title catch a missing or empty <title>. Whether a title describes the page, and whether a single-page app updates it, needs manual review: tab through a few pages and read the titles.

2.4.3 Focus Order

Level A · EN 301 549 9.2.4.3 (V3.2.1 and V4.1.1)

Rule. When you move through a page with Tab, focus follows an order that keeps its meaning and lets you operate it.

Fails: 2.4.3 Focus Order, HTML
<label for="email">Email</label><input id="email" type="email" autocomplete="email" tabindex="2"><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" tabindex="1"><button tabindex="3">Sign in</button>
Passes: 2.4.3 Focus Order, HTML
<label for="email">Email</label><input id="email" type="email" autocomplete="email"><label for="password">Password</label><input id="password" type="password" autocomplete="current-password"><button>Sign in</button>

Why it works. Positive tabindex values pull elements ahead of everything else on the page in a separate sequence. Removing them makes focus follow the source order, which matches the visual order here. For dialogs, open them with dialog.showModal(), which moves focus inside.

Can automated tools catch this? Partly. Our rule focus-order reports when Tab moves to an element that comes earlier in the document than the previous stop. axe-core 4.14.0 has no WCAG-tagged rule for this criterion (its tabindex rule is a best practice). Whether focus moves into and back out of dialogs and menus sensibly needs manual review.

2.4.4 Link Purpose (In Context)

Level A · EN 301 549 9.2.4.4 (V3.2.1 and V4.1.1)

Rule. The purpose of each link is clear from its text, or from its text together with its context in code: the same sentence, paragraph, list item or table cell. Links that would be ambiguous to everyone are exempt.

Fails: 2.4.4 Link Purpose (In Context), HTML
<ul class="products">  <li>    <img src="/img/rain-jacket.jpg" alt="">    <a href="/p/rain-jacket">Click here</a>  </li></ul><a href="/cart"><img src="/icons/cart.svg" alt=""></a>
Passes: 2.4.4 Link Purpose (In Context), HTML
<ul class="products">  <li>    <img src="/img/rain-jacket.jpg" alt="">    <a href="/p/rain-jacket">Packable rain jacket</a>  </li></ul><a href="/cart"><img src="/icons/cart.svg" alt="Cart"></a>

Why it works. Screen reader users often list a page’s links on their own, where “Click here” says nothing. The image link gets its name from the icon’s alt.

Can automated tools catch this? Partly. axe-core’s link-name and area-alt flag links with no accessible text at all. Our rule link-text-not-descriptive reports links with no name, a bare URL, generic wording such as “click here” or “read more”, or one label used for several destinations. It reads the text without its context, so a person has to decide whether the sentence or list item around a flagged link makes it clear.

2.4.5 Multiple Ways

Level AA · EN 301 549 9.2.4.5 (V3.2.1 and V4.1.1)

Rule. There is more than one way to find a page within a site: for example navigation plus search, a site map or an A–Z index. Pages that are the result of, or a step in, a process, such as checkout, are exempt.

Fails: 2.4.5 Multiple Ways, HTML
<header>  <nav aria-label="Main"><!-- the only route to every page --></nav></header>
Passes: 2.4.5 Multiple Ways, HTML
<header>  <nav aria-label="Main"><!-- navigation links --></nav>  <form role="search" action="/search">    <label for="q">Search the site</label>    <input id="q" name="q" type="search">    <button>Search</button>  </form></header><footer>  <a href="/sitemap">Site map</a></footer>

Why it works. Some people find pages faster by searching, others by browsing. A second route also helps people who struggle with deep menus.

Can automated tools catch this? No. This is manual only: it is a property of the whole site, not of one page.

2.4.6 Headings and Labels

Level AA · EN 301 549 9.2.4.6 (V3.2.1 and V4.1.1)

Rule. Headings and labels describe their topic or purpose. The criterion does not require headings or labels to exist (1.3.1 and 3.3.2 cover that); it requires the ones you have to be descriptive.

Fails: 2.4.6 Headings and Labels, HTML
<h2>More</h2><label for="f1">Field 1</label><input id="f1" name="f1">
Passes: 2.4.6 Headings and Labels, HTML
<h2>Delivery and returns</h2><label for="postcode">Postcode</label><input id="postcode" name="postcode" autocomplete="postal-code">

Why it works. People skim by headings and labels, and screen reader users jump between them. “More” and “Field 1” give them nothing to go on.

Can automated tools catch this? No. This is manual only: whether a heading or label describes its content is a reading task. (axe-core’s empty-heading is a best-practice rule, not tagged for this criterion.)

2.4.7 Focus Visible

Level AA · EN 301 549 9.2.4.7 (V3.2.1 and V4.1.1)

Rule. Whatever has keyboard focus shows a visible focus indicator.

Fails: 2.4.7 Focus Visible, CSS
*:focus { outline: none; }
Passes: 2.4.7 Focus Visible, CSS
:focus-visible {  outline: 3px solid #1a56db;  outline-offset: 2px;}

Why it works. :focus-visible shows the ring when the browser judges it useful, such as during keyboard navigation, so removing focus styles “for mouse users” is no longer needed. A thick outline offset from the element stays visible on most backgrounds.

Can automated tools catch this? Partly. Our rule focus-indicator-missing reports controls whose outline, shadow, border colour and background colour all stay the same when they take focus. axe-core 4.14.0 has no rule for this criterion. Whether the indicator can be seen on every background, and inside composite widgets, needs manual review.

2.4.11 Focus Not Obscured (Minimum)

Level AA · New in 2.2 · EN 301 549 9.2.4.11 (V4.1.1 only)

Rule. When a control receives keyboard focus, it is not entirely hidden by content the author created, such as a sticky header, a cookie banner or a chat launcher. Partly covered passes at this level. Content the user opened, such as a non-modal dialog, may cover it if the user can reveal the control without moving focus, for example by pressing Esc. For content the user can move, only its starting position counts.

Fails: 2.4.11 Focus Not Obscured (Minimum), CSS
.site-header { position: sticky; top: 0; z-index: 1; height: 80px; background: #fff; }.cookie-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 120px; background: #fff; }
Passes: 2.4.11 Focus Not Obscured (Minimum), CSS
.site-header { position: sticky; top: 0; z-index: 1; height: 80px; background: #fff; }.cookie-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 120px; background: #fff; }html {  scroll-padding-top: 88px;  scroll-padding-bottom: 128px;}body { padding-bottom: 120px; }

Why it works. When focus moves to an element off screen, the browser scrolls it into view. scroll-padding tells it to keep that element clear of the areas the sticky header and the fixed banner cover, so a focused link is scrolled clear of both, and the bottom padding lets the last links on the page scroll above the banner. W3C lists this as technique C43. For more patterns, see how sticky headers and cookie banners hide focus.

Can automated tools catch this? Partly. Our rule focus-obscured reports a control that, when Tab moves focus to it, is covered by other content at every one of five points that is on screen: its centre and four points just inside its corners. From each control we also press Shift+Tab once and check the control before it again, but only if the page has moved it since Tab reached it, and only for a fixed or sticky cover such as a header. We do not walk all the way back up. While we press Tab, CSS smooth scrolling is switched to instant; smooth scrolling that a script explicitly requests is not. axe-core 4.14.0 has no rule for 2.4.11. A control hidden only after several Shift+Tab presses in a row, a cover moved by script-requested smooth scrolling, a consent banner before the visitor answers it, banners that appear later in a session, other screen widths, and user-opened content need manual review. (Partial overlap is a Level AAA question, 2.4.12.)

2.5.1 Pointer Gestures

Level A · EN 301 549 9.2.5.1 (V3.2.1 and V4.1.1)

Rule. Anything done with a multi-finger gesture (pinch, two-finger swipe) or a path-based gesture (swiping along a direction, drawing a shape) can also be done with a single pointer without a path, such as a tap or click, unless the gesture is essential.

Fails: 2.5.1 Pointer Gestures, JavaScript
let startX = 0;gallery.addEventListener("touchstart", (event) => { startX = event.touches[0].clientX; });gallery.addEventListener("touchend", (event) => {  if (event.changedTouches[0].clientX - startX < -50) showNext();});
Passes: 2.5.1 Pointer Gestures, HTML
<button type="button" id="prev-photo">Previous photo</button><button type="button" id="next-photo">Next photo</button><script>  document.querySelector("#prev-photo").addEventListener("click", showPrevious);  document.querySelector("#next-photo").addEventListener("click", showNext);</script>

Why it works. Keep the swipe for people who like it. The buttons serve people who use a head pointer, a mouth stick or a tremor-stabilising mouse, for whom a precise swipe is hard or impossible.

Can automated tools catch this? No. This is manual only: try each gesture feature with single taps and clicks only.

2.5.2 Pointer Cancellation

Level A · EN 301 549 9.2.5.2 (V3.2.1 and V4.1.1)

Rule. A single-pointer action does not complete on the down event. It completes on release, and moving away before releasing cancels it; or it can be undone; or releasing reverses what the press did, as with a preview that shows only while you hold it. The exception is when a down-event action is essential, such as a piano key.

Fails: 2.5.2 Pointer Cancellation, JavaScript
deleteButton.addEventListener("pointerdown", deleteItem);
Passes: 2.5.2 Pointer Cancellation, JavaScript
deleteButton.addEventListener("click", deleteItem);

Why it works. click fires on release, and only if the pointer is still on the button, so someone who presses the wrong thing can slide off before letting go.

Can automated tools catch this? No. This is manual only: a script can find pointerdown or mousedown handlers, but not whether they complete an action.

2.5.3 Label in Name

Level A · EN 301 549 9.2.5.3 (V3.2.1 and V4.1.1)

Rule. When a control has a visible text label, its accessible name contains that text, ideally at the start.

Fails: 2.5.3 Label in Name, HTML
<button aria-label="Submit form">Send message</button>
Passes: 2.5.3 Label in Name, HTML
<button>Send message</button>

Why it works. A speech-input user says what they see: “click Send message”. That only works if the name the software knows includes those words. aria-label replaces the visible text as the name, so leave it off, or start it with the visible words.

Can automated tools catch this? Partly. axe-core’s label-content-name-mismatch compares the visible text with the accessible name, word by word; since axe-core 4.14.0 it runs by default. Labels next to a control and text in images need manual review.

2.5.4 Motion Actuation

Level A · EN 301 549 9.2.5.4 (V3.2.1 and V4.1.1)

Rule. Anything triggered by moving the device (shaking, tilting) or by gestures to a camera can also be done with an ordinary control, and the motion response can be turned off. The exceptions: the motion is used through an accessibility-supported interface, such as the movements needed to operate assistive technology, or motion is essential, such as a step counter.

Fails: 2.5.4 Motion Actuation, JavaScript
window.addEventListener("devicemotion", (event) => {  const a = event.acceleration;  if (a && Math.abs(a.x) > 15) undoLastAction();});
Passes: 2.5.4 Motion Actuation, HTML
<button type="button" id="undo">Undo</button><label><input type="checkbox" id="shake-undo"> Shake to undo</label><script>  document.querySelector("#undo").addEventListener("click", undoLastAction);  window.addEventListener("devicemotion", (event) => {    if (!document.querySelector("#shake-undo").checked) return;    const a = event.acceleration;    if (a && Math.abs(a.x) > 15) undoLastAction();  });</script>

Why it works. The Undo button works for someone whose device is mounted, and shake-to-undo is off until the user turns it on, so tremors cannot trigger it.

Can automated tools catch this? No. This is manual only.

2.5.7 Dragging Movements

Level AA · New in 2.2 · EN 301 549 9.2.5.7 (V4.1.1 only)

Rule. Anything done by dragging, such as reordering a list, moving a slider thumb or panning a map, can also be done with a single pointer without dragging: taps or clicks. Dragging that is essential, and dragging handled by the browser and not changed by the author, are exempt. A keyboard alternative alone does not meet this criterion; the alternative has to work with a pointer.

Fails: 2.5.7 Dragging Movements, HTML
<!-- reordered only by drag and drop in script --><ul id="priorities">  <li draggable="true">Fix checkout errors</li>  <li draggable="true">Update the pricing page</li></ul>
Passes: 2.5.7 Dragging Movements, HTML
<ul id="priorities">  <li>    <span>Fix checkout errors</span>    <button type="button" class="move" data-dir="up"            aria-label="Move Fix checkout errors up">Up</button>    <button type="button" class="move" data-dir="down"            aria-label="Move Fix checkout errors down">Down</button>  </li>  <li>    <span>Update the pricing page</span>    <button type="button" class="move" data-dir="up"            aria-label="Move Update the pricing page up">Up</button>    <button type="button" class="move" data-dir="down"            aria-label="Move Update the pricing page down">Down</button>  </li></ul><script>  document.querySelector("#priorities").addEventListener("click", (event) => {    const button = event.target.closest("button.move");    if (!button) return;    const item = button.closest("li");    if (button.dataset.dir === "up" && item.previousElementSibling) {      item.previousElementSibling.before(item);    } else if (button.dataset.dir === "down" && item.nextElementSibling) {      item.nextElementSibling.after(item);    }    button.focus();  });</script>

Why it works. Up and Down buttons move an item with single taps, so people who cannot hold a press while moving can still reorder. Dragging can stay as well. Focus returns to the pressed button after the move, so keyboard users keep their place too, and each name contains its visible word, Up or Down, so speech users can say it (2.5.3).

Sliders, sortable lists and maps each need a different fix; we cover single-pointer alternatives to dragging pattern by pattern.

Can automated tools catch this? No. This is manual only. axe-core 4.14.0 has no rule for it, and we have no automated check for it either. Find every drag interaction and try to complete it with clicks alone.

2.5.8 Target Size (Minimum)

Level AA · New in 2.2 · EN 301 549 9.2.5.8 (V4.1.1 only)

Rule. Pointer targets are at least 24 by 24 CSS pixels, with five exceptions:

  • Spacing: a smaller target passes if a circle 24 CSS pixels in diameter, centred on its bounding box, does not intersect another target or another undersized target’s circle.
  • Equivalent: another control on the same page does the same thing and meets the size.
  • Inline: the target sits in a sentence, or its size is set by the line height of the text around it.
  • User agent control: the browser sets the size and the author has not changed it.
  • Essential: a particular presentation is essential or legally required, such as pins on a map.

The spacing exception has its own geometry; we work through measuring a 24 by 24 target and its spacing circle.

Fails: 2.5.8 Target Size (Minimum), CSS
.pagination a {  display: inline-block;  width: 16px;  height: 16px;  margin: 0 2px;  font-size: 12px;}
Passes: 2.5.8 Target Size (Minimum), CSS
.pagination a {  display: inline-flex;  align-items: center;  justify-content: center;  min-width: 24px;  min-height: 24px;}

Why it works. The links are now at least 24 pixels square. The failing version also had no spacing to fall back on: 16-pixel links 4 pixels apart (with no whitespace between the links in the markup) put their centres 20 pixels apart, so their 24-pixel circles overlap. Keeping 16 pixels with centres at least 24 pixels apart would pass through the spacing exception, but the larger target is easier for everyone.

Can automated tools catch this? Partly. axe-core’s target-size checks size and spacing, but it is off in axe-core’s default configuration and runs only when a tool turns it on: by naming it, or by selecting a tag it carries, such as wcag22aa, wcag258 or EN-301-549v4. Our AA audits run it. Our rule touch-target-minimum reports controls under 24 pixels that fail the 24-pixel circle test described above, at each screen size we test. It skips native checkbox, radio, file, range and colour inputs the browser draws at its default size; checkbox, radio, file and colour inputs whose visible label is at least 24 by 24 pixels; a control with the same label or text as a control of at least 24 by 24 pixels; controls in recognised carousel or ticker tracks while moving, clipped, invisible or covered by their track’s copies; and every link inside a paragraph or list item, which it treats as inline. So check small links in lists, such as pagination and menus, and controls in carousels by hand. The inline, equivalent and essential exceptions need manual review.

Understandable

People have to be able to read the content, predict what the interface will do, and recover from mistakes, especially in forms and sign-in.

3.1.1 Language of Page

Level A · EN 301 549 9.3.1.1 (V3.2.1 and V4.1.1)

Rule. The page declares its main language in code.

Fails: 3.1.1 Language of Page, HTML
<html>
Passes: 3.1.1 Language of Page, HTML
<html lang="en">

Why it works. Screen readers choose their pronunciation rules from lang, and browsers use it for translation, hyphenation and spelling. A Danish page read with English rules is close to unintelligible.

Can automated tools catch this? Partly. axe-core’s html-has-lang, html-lang-valid and html-xml-lang-mismatch catch a missing, invalid or conflicting language. Whether the declared language is the one the page is actually written in, which templates copied between sites often get wrong, needs manual review.

3.1.2 Language of Parts

Level AA · EN 301 549 9.3.1.2 (V3.2.1 and V4.1.1)

Rule. Passages and phrases in a different language from the page are marked with their own language. Proper names, technical terms, words of no particular language, and words that have become part of the surrounding language are exempt.

Fails: 3.1.2 Language of Parts, HTML
<p>Our design principle is <em>hygge for alle</em>.</p>
Passes: 3.1.2 Language of Parts, HTML
<p>Our design principle is <em lang="da">hygge for alle</em>.</p>

Why it works. The screen reader switches to Danish pronunciation for the phrase and back to English afterwards. A language switcher’s options benefit the same way: <a href="/da/" lang="da">Dansk</a>.

Can automated tools catch this? Partly. axe-core’s valid-lang checks that lang values are valid codes. It cannot tell that a phrase is in another language and has no lang; that needs manual review.

3.2.1 On Focus

Level A · EN 301 549 9.3.2.1 (V3.2.1 and V4.1.1)

Rule. Moving focus to something does not by itself change the context: no new page, no new window, no focus jump, no form submission.

Fails: 3.2.1 On Focus, HTML
<a href="/help" id="help-link">Help</a><script>  document.querySelector("#help-link").addEventListener("focus", () => {    window.open("/help", "help");  });</script>
Passes: 3.2.1 On Focus, HTML
<a href="/help">Help</a>

Why it works. A keyboard user who tabs through the page now passes the link without anything happening, and opens help only by pressing Enter.

Can automated tools catch this? Partly. Our rule focus-changes-context presses Tab through the page and reports a control that loads another page or opens a new window when it takes focus. axe-core 4.14.0 has no rule for it. A control that rewrites the page in place or moves focus elsewhere needs manual review: tab through the page and note anything that changes when focus arrives rather than when you press a key.

3.2.2 On Input

Level A · EN 301 549 9.3.2.2 (V3.2.1 and V4.1.1)

Rule. Changing a setting, such as choosing an option, ticking a box or typing in a field, does not automatically change the context, unless the user was told beforehand that it would.

Fails: 3.2.2 On Input, HTML
<label for="lang">Language</label><select id="lang" onchange="location.href = '/' + this.value + '/'">  <option value="en">English</option>  <option value="da">Dansk</option></select>
Passes: 3.2.2 On Input, HTML
<form action="/language" method="get">  <label for="lang">Language</label>  <select id="lang" name="lang">    <option value="en">English</option>    <option value="da" lang="da">Dansk</option>  </select>  <button>Change language</button></form>

Why it works. Keyboard users move through a <select> with the arrow keys, and depending on the browser and platform each press can change the value and fire change, so the failing version can reload the page before they reach their choice. A button lets them choose first and act second.

Can automated tools catch this? No. This is manual only: change every select, radio group and checkbox with the keyboard and watch for page loads, focus jumps or submissions.

3.2.3 Consistent Navigation

Level AA · EN 301 549 9.3.2.3 (V3.2.1 and V4.1.1)

Rule. Navigation that repeats across pages appears in the same relative order each time, unless the user changes it. Adding or removing items is fine; reshuffling them is not.

Fails (two templates with their own hard-coded menus):

Fails: 3.2.3 Consistent Navigation, HTML
<!-- product page --><nav aria-label="Main">  <a href="/products">Products</a>  <a href="/pricing">Pricing</a>  <a href="/support">Support</a></nav><!-- support page --><nav aria-label="Main">  <a href="/support">Support</a>  <a href="/products">Products</a>  <a href="/pricing">Pricing</a></nav>

Passes (one list, rendered by every template):

Passes: 3.2.3 Consistent Navigation, JavaScript
const MAIN_NAV = [  { href: "/products", label: "Products" },  { href: "/pricing", label: "Pricing" },  { href: "/support", label: "Support" },];​function renderMainNav(currentPath) {  const links = MAIN_NAV.map(({ href, label }) => {    const current = href === currentPath ? ' aria-current="page"' : "";    return `<a href="${href}"${current}>${label}</a>`;  });  return `<nav aria-label="Main">${links.join(" ")}</nav>`;}

Why it works. A single source means the order cannot drift between templates. aria-current marks where you are without moving anything.

Can automated tools catch this? Partly. Our rule consistent-navigation compares the order of the links in the header, navigation and footer landmarks that repeat across the pages an audit captured, and reports links that come in a different relative order. axe-core 4.14.0 has no rule for it, because it looks at one page at a time. Where blocks such as a search field sit, navigation outside those landmarks, and pages the audit did not capture need manual review: compare the navigation on one page of each template.

3.2.4 Consistent Identification

Level AA · EN 301 549 9.3.2.4 (V3.2.1 and V4.1.1)

Rule. Components that do the same thing within a set of pages are identified consistently: a consistent name, label, text alternative and icon. Consistent is not identical: a “next page” arrow can have the text alternative “Go to page 4” on page 3 and “Go to page 5” on page 4.

Fails: 3.2.4 Consistent Identification, HTML
<!-- on /products --><button type="submit"><img src="/icons/search.svg" alt="Search"></button><!-- on /support --><button type="submit"><img src="/icons/search.svg" alt="Find"></button>

Passes (both pages):

Passes: 3.2.4 Consistent Identification, HTML
<button type="submit"><img src="/icons/search.svg" alt="Search"></button>

Why it works. People learn a site’s controls once. A screen reader user who heard “Search” on one page should not have to work out that “Find” is the same thing on the next.

Can automated tools catch this? Partly. Our rule consistent-identification compares the names of links to the same address in the header, navigation and footer landmarks that repeat across the pages an audit captured, and reports a link named differently on some of them. axe-core 4.14.0 has no rule for it. Buttons, icons and other links, and whether two different names still mean the same thing, need manual review across pages.

3.2.6 Consistent Help

Level A · New in 2.2 · EN 301 549 9.3.2.6 (V4.1.1 only)

Rule. If help (contact details, a contact form or chat, a self-help page, or an automated assistant) is repeated on several pages within a set of pages, it comes in the same order relative to the other content those pages share, unless the user changes it. The criterion does not require you to offer help, and leaving it off a page is not a failure.

Fails: 3.2.6 Consistent Help, HTML
<!-- /products/jacket: the contact link in the header --><header>  <nav aria-label="Main"><!-- navigation links --></nav>  <a href="/contact">Contact us</a></header><main><!-- product details --></main><footer><!-- footer links --></footer>​<!-- /products/boots: the same link moved to the footer --><header>  <nav aria-label="Main"><!-- navigation links --></nav></header><main><!-- product details --></main><footer>  <a href="/contact">Contact us</a></footer>

Passes (both product pages):

Passes: 3.2.6 Consistent Help, HTML
<!-- /products/jacket and /products/boots alike --><header>  <nav aria-label="Main"><!-- navigation links --></nav>  <a href="/contact">Contact us</a></header><main><!-- product details --></main><footer><!-- footer links --></footer>

Why it works. People who need help, often because they are stuck, find it where it was on the last page. Keeping it in checkout too is good practice, though the criterion only governs order where help repeats.

Can automated tools catch this? No. This is manual only: axe-core 4.14.0 has no rule for it, and we have no automated check for it either. Compare where help sits on pages of the same set.

3.3.1 Error Identification

Level A · EN 301 549 9.3.3.1 (V3.2.1 and V4.1.1)

Rule. When an input error is detected automatically, the field in error is identified and the error is described in text.

Fails: 3.3.1 Error Identification, HTML
<label for="email">Email</label><input id="email" name="email" type="email" class="has-error"><!-- the only sign of the error is a red border -->
Passes: 3.3.1 Error Identification, HTML
<label for="email">Email</label><input id="email" name="email" type="email"       aria-invalid="true" aria-describedby="email-error"><p id="email-error" class="field-error">Enter an email address, like name@example.com.</p>

Why it works. The message says in words what is wrong and identifies the field it is about, which is what this criterion asks for. aria-invalid marks the field as in error and aria-describedby makes a screen reader read the message with the field when it receives focus. Both are good practice, and W3C’s guidance says this criterion does not require that programmatic link. We count a message tied to its field only by where it sits on screen under 1.3.1 Info and Relationships.

Can automated tools catch this? No. This is manual only: axe-core 4.14.0 has no rule for it, and we have no check that decides it. Our form check reports only where an error message is already on screen, so it reports under 1.3.1, not here. Submit each form empty and with wrong values, including errors the server returns, and check that every error identifies the field and says in words what is wrong.

3.3.2 Labels or Instructions

Level A · EN 301 549 9.3.3.2 (V3.2.1 and V4.1.1)

Rule. Fields that need input have labels or instructions. Where it matters, that includes the expected format and which fields are required.

Fails: 3.3.2 Labels or Instructions, HTML
<input type="text" name="dob" placeholder="Date of birth">
Passes: 3.3.2 Labels or Instructions, HTML
<label for="dob">Date of birth</label><p id="dob-hint">Use the format DD/MM/YYYY, for example 27/03/1985.</p><input id="dob" name="dob" type="text" autocomplete="bday"       aria-describedby="dob-hint">

Why it works. No instruction gives the format, and a placeholder disappears as you type, so the only label is gone while you need it; its grey text is usually too faint as well. A real label stays, and the hint gives the format before an error does.

Can automated tools catch this? Partly. axe-core’s form-field-multiple-labels marks a field with more than one label for review, never as a failure, and its label rule, filed under 4.1.2, flags a field with none. axe counts a placeholder as a label, so the failing example above passes both rules. Whether the labels and instructions are enough needs manual review.

3.3.3 Error Suggestion

Level AA · EN 301 549 9.3.3.3 (V3.2.1 and V4.1.1)

Rule. When an input error is detected and a correction is known, the suggestion is given to the user, unless that would put security or the purpose of the content at risk.

Fails: 3.3.3 Error Suggestion, HTML
<p id="dob-error" class="field-error">Invalid input.</p>
Passes: 3.3.3 Error Suggestion, HTML
<p id="dob-error" class="field-error">Enter a date in the past, in the format DD/MM/YYYY.</p>

Why it works. The message tells people how to fix the problem, not only that there is one. The security exception covers cases like a password field, where you would not say which part of a sign-in was wrong.

Can automated tools catch this? No. This is manual only: judging whether a message suggests a fix means reading it.

3.3.4 Error Prevention (Legal, Financial, Data)

Level AA · EN 301 549 9.3.3.4 (V3.2.1 and V4.1.1)

Rule. On pages that create a legal commitment or a financial transaction, modify or delete data the user controls in a data storage system, or submit test answers, at least one of these holds: the submission can be reversed, the input is checked and the user can correct it, or the user can review and confirm before it is final.

Fails: 3.3.4 Error Prevention (Legal, Financial, Data), HTML
<!-- no review step, no input check, no way to cancel the order --><form method="post" action="/checkout/pay">  <!-- card fields -->  <button>Pay</button></form>
Passes: 3.3.4 Error Prevention (Legal, Financial, Data), HTML
<h1>Check your order</h1><dl>  <dt>Item</dt>  <dd>Packable rain jacket, size M <a href="/basket">Change</a></dd>  <dt>Deliver to</dt>  <dd>The address you entered <a href="/checkout/delivery">Change</a></dd>  <dt>Total</dt>  <dd>€89.00</dd></dl><form method="post" action="/checkout/pay">  <button>Confirm and pay €89.00</button></form>

Why it works. A review step catches the wrong size or the old address before money moves, and the button says exactly what pressing it will do.

Can automated tools catch this? No. This is manual only: complete each purchase, booking or deletion flow and check for a review, a check or an undo.

3.3.7 Redundant Entry

Level A · New in 2.2 · EN 301 549 9.3.3.7 (V4.1.1 only)

Rule. Information the user already entered, or was given, earlier in the same process is filled in for them or offered for them to select, instead of asked for again. The exceptions: re-entering is essential (as in a memory game), the information is needed for security (such as confirming a password), or the earlier information is no longer valid.

Fails: 3.3.7 Redundant Entry, HTML
<h2>Billing address</h2><label for="b-street">Street</label><input id="b-street" name="billing_street" autocomplete="billing street-address"><!-- the user typed the same address one step earlier, for delivery -->
Passes: 3.3.7 Redundant Entry, HTML
<fieldset>  <legend>Billing address</legend>  <label>    <input type="checkbox" name="billing_same" value="yes" checked>    Same as delivery address  </label></fieldset>

Why it works. People with memory or motor impairments do not have to recall and retype what they already gave you. Unticking the box can reveal the empty fields for anyone whose billing address differs.

Can automated tools catch this? No. This is manual only: axe-core 4.14.0 has no rule for it, and we have no automated check for it either. It is tested by walking a whole process and noting every question asked twice.

We walk a checkout for redundant entry, and compare help across templates for 3.2.6, in redundant entry and consistent help in WCAG 2.2.

3.3.8 Accessible Authentication (Minimum)

Level AA · New in 2.2 · EN 301 549 9.3.3.8 (V4.1.1 only)

Rule. No step of a sign-in process requires a cognitive function test, such as remembering a password, transcribing a code or solving a puzzle, unless that step offers at least one of: another method that does not rely on such a test; a mechanism that helps, such as a password manager filling the field or copy and paste; a test that only asks to recognise objects; or a test that asks to identify non-text content, such as a photo, that the user provided to the site. The last two exceptions apply at Level AA only; Level AAA (3.3.9) removes them.

Fails: 3.3.8 Accessible Authentication (Minimum), HTML
<label for="password">Password</label><input id="password" name="password" type="password"       autocomplete="off" onpaste="return false">
Passes: 3.3.8 Accessible Authentication (Minimum), HTML
<label for="username">Email</label><input id="username" name="username" type="email" autocomplete="username"><label for="password">Password</label><input id="password" name="password" type="password" autocomplete="current-password"><label for="otp">Sign-in code</label><input id="otp" name="otp" inputmode="numeric" autocomplete="one-time-code">

Why it works. With autofill allowed and paste working, a password manager can supply the password and people can paste a code instead of copying it digit by digit. autocomplete="one-time-code" lets some phones offer a code received by text message. Blocking paste fails the criterion unless another method is offered, as W3C’s Understanding document for 3.3.8 says; WCAG failure F109 covers the related case of a code split across separate fields. autocomplete="off" asks the browser not to fill the field; in most browsers it does not stop a password manager filling a sign-in form, but remove it anyway.

We compare sign-in flows that pass accessible authentication, from passkeys to email links, in a separate guide.

Can automated tools catch this? Partly. Our rule accessible-authentication reports credential fields whose markup blocks paste (an inline onpaste handler on the field or its form) as a high finding. autocomplete="off" alone on a password field is a low finding and leaves 3.3.8 as cannot tell, since browsers usually ignore it on password fields. A paste block added by a script listener is not detected. axe-core 4.14.0 has no rule for this criterion. CAPTCHAs, puzzles, codes split across fields that block paste, and steps behind the first screen of a sign-in need manual review.

Robust

Content has to work with the browsers and assistive technologies people use now and later, which mostly comes down to controls that say what they are and what state they are in. 4.1.1 Parsing, the first criterion under this principle in WCAG 2.0 and 2.1, is obsolete in 2.2 (see where 4.1.1 Parsing went).

4.1.2 Name, Role, Value

Level A · EN 301 549 9.4.1.2 (V3.2.1 and V4.1.1)

Rule. Every control, including custom ones built with scripts, exposes its name and role to assistive technology, lets its states and values be set where the user can set them, and reports changes to them.

Fails: 4.1.2 Name, Role, Value, HTML
<div class="toggle on" onclick="toggleDarkMode(this)"></div><button>  <svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg></button>
Passes: 4.1.2 Name, Role, Value, HTML
<button type="button" role="switch" aria-checked="true" id="dark-mode">  Dark mode</button><button type="button" id="menu-button" aria-label="Menu"        aria-expanded="false" aria-controls="site-menu">  <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">    <path d="M3 6h18M3 12h18M3 18h18"/>  </svg></button><nav id="site-menu" aria-label="Main" hidden><!-- navigation links --></nav><script>  const toggle = document.querySelector("#dark-mode");  toggle.addEventListener("click", () => {    const on = toggle.getAttribute("aria-checked") !== "true";    toggle.setAttribute("aria-checked", String(on));    document.documentElement.classList.toggle("dark", on);  });  const menuButton = document.querySelector("#menu-button");  const menu = document.querySelector("#site-menu");  menuButton.addEventListener("click", () => {    const open = menuButton.getAttribute("aria-expanded") !== "true";    menuButton.setAttribute("aria-expanded", String(open));    menu.hidden = !open;  });</script>

Why it works. A screen reader announces the switch with its name, its role and whether it is on, and announces the change when it is pressed. The menu button has a name, and aria-expanded tells a screen reader whether the menu is open, changing as it opens and closes.

Can automated tools catch this? Partly. axe-core has over twenty rules tagged for 4.1.2, among them button-name, link-name, label, select-name, aria-required-attr, aria-valid-attr-value, aria-hidden-focus, nested-interactive and frame-title. Our rules control-label, mobile-menu-state and dialog-state report unlabelled form controls, a mobile menu trigger that does not expose whether the menu is open, and a dialog that does not expose whether it is open. Whether a custom widget reports every state change correctly needs manual review with a screen reader.

4.1.3 Status Messages

Level AA · EN 301 549 9.4.1.3 (V3.2.1 and V4.1.1)

Rule. Status messages, such as “Added to cart”, “12 results” or “Saving…”, are exposed through a role or property, so assistive technology can present them without moving focus. A message that takes focus is not a status message.

Fails: 4.1.3 Status Messages, HTML
<button type="button" id="add">Add to cart</button><p class="cart-message"></p><script>  document.querySelector("#add").addEventListener("click", () => {    document.querySelector(".cart-message").textContent =      "Added to cart. 3 items in your cart.";  });</script>
Passes: 4.1.3 Status Messages, HTML
<button type="button" id="add">Add to cart</button><p class="cart-message" role="status"></p><script>  document.querySelector("#add").addEventListener("click", () => {    document.querySelector(".cart-message").textContent =      "Added to cart. 3 items in your cart.";  });</script>

Why it works. role="status" makes the element a polite live region: when its text changes, a screen reader reads it out at the next pause. The region has to be in the page before the text arrives, which is why the role sits on the empty element. Use role="alert" only for urgent messages such as errors.

Can automated tools catch this? Partly. Our rule status-message-not-announced reports a new or changed form message without a changed live region carrying it. We compare linked and form text before and after emptying a required field or mistyping an email or URL. Unchanged hints do not count; unlinked live text is inferred within the form. We do not observe focus or screen-reader announcements. It runs in the same form check as form-error-not-identified, which is off unless you switch it on. axe-core 4.14.0 has no rule for this criterion. Cart updates, search result counts and toasts need manual review with a screen reader.

Check a page against this list

The fastest way to start is the automated layer. Scan one page of your site free, without signing up. You get that page’s measured findings, each labelled with the WCAG criterion and EN 301 549 clause its rule maps to where it maps to one. Then work through the manual criteria above by hand.

Frequently asked questions

How many success criteria are in WCAG 2.2 Level AA?

55: 31 at Level A and 24 at Level AA. WCAG 2.2 has 86 criteria in all; the other 31 are Level AAA. 4.1.1 Parsing is obsolete and is not counted.

Is 4.1.1 Parsing still required?

Not under WCAG 2.2, which removed it. It is still part of WCAG 2.0 and 2.1, and of EN 301 549 V3.2.1, the version cited today under the Web Accessibility Directive. W3C notes that authors required by policy to conform to 2.0 or 2.1 “may need to continue to test and report 4.1.1”. WCAG 2.1 now notes it should be considered always satisfied for HTML or XML content.

Is WCAG 2.2 backwards compatible with WCAG 2.1?

Yes. W3C states that content that conforms to WCAG 2.2 also conforms to WCAG 2.0 and 2.1. The one gap is 4.1.1, which 2.2 no longer tests, so keep it on your list where a 2.0 or 2.1 requirement applies.

Can an automated scan tell me whether my site meets WCAG 2.2 AA?

No. Automated rules catch some failures of some criteria, and many criteria are manual only, as the table and the detection notes show. A scan with no findings means nothing was found by the checks that ran. Meeting EN 301 549 in full also requires testing with assistive technologies.

Does the European Accessibility Act require WCAG 2.2?

The Act names no WCAG version. A harmonised standard cited in the Official Journal gives a presumption of conformity, and none is cited under the Act yet. EN 301 549 V4.1.1, which adopts WCAG 2.2 Level AA, awaits citation; V3.2.1, which adopts WCAG 2.1 Level AA, is cited under the Web Accessibility Directive. Your national law may say more, so check it.