Skip to content
inklu
wcagwcag-2-2accessibilityengineeringcompliance

The nine new WCAG 2.2 success criteria, decoded for engineers

A working developer's read on the nine success criteria WCAG 2.2 added on top of 2.1 — what they require, how they fail, and what the fix looks like in code.

By Moe· Co-founder, Design10 min read

A reader sent us a screen recording last week. A user on a phone tries to submit a checkout form. Focus lands on "Place order." A cookie banner slides up and parks on top of it. The user cannot see what is focused, tabs forward, scrolls, hits "Accept all" by mistake, and the page reloads. Cart abandoned.

That specific failure has a name in WCAG 2.2. It is 2.4.11, Focus Not Obscured (Minimum), and it was not in 2.1. If you are still building against the 2.1 checklist, you will ship that bug and pass your own audit.

WCAG 2.2 was published in October 2023 as a W3C Recommendation, added nine new success criteria on top of 2.1, and removed one (4.1.1 Parsing, the long-suffering "valid HTML" criterion that browsers had quietly made irrelevant). It is the version cited in the EAA's harmonized standard EN 301 549, the version most US procurement teams now require, and the version inklu scans against. If you only learn one accessibility spec this year, learn 2.2.

This post walks through each of the nine new criteria in the order they appear in the spec. For each one I want to give you the same three things: what it actually says, the most common way I see it fail in production, and the smallest change in code that gets you to compliant. No padding.

2.4.11 Focus Not Obscured (Minimum) — Level AA

When a user-interface component receives keyboard focus, the component is not entirely hidden by author-created content. Note the word "entirely." If half the button is behind your cookie banner, you are still compliant on this one. The Enhanced version (2.4.12, AAA) tightens that to "not hidden at all."

Where this breaks: sticky headers, sticky footers, cookie banners, support chat widgets, "subscribe" interstitials, video PiP players. Anything you position: fixed to the viewport edge.

The fix is almost never to remove the sticky element. It is to make sure focused elements scroll into a region the sticky element does not cover. Two patterns work:

/* 1. Reserve the sticky region with scroll-margin */
:focus-visible {
  scroll-margin-top: 72px;     /* height of sticky header */
  scroll-margin-bottom: 96px;  /* height of cookie banner */
}
// 2. On focus, scroll the element into the safe zone
element.addEventListener('focus', () => {
  element.scrollIntoView({ block: 'center', behavior: 'smooth' })
})

The scroll-margin approach is usually enough on its own and costs you no JavaScript. The catch: it only kicks in when the browser is the one scrolling (tab navigation, anchor links). If your sticky element appears after a user has already focused something, you have a separate problem. Move the banner trigger to before paint, or push focused content up programmatically when the banner mounts.

2.4.12 Focus Not Obscured (Enhanced) — Level AAA

Same idea, stricter bar. No part of the focused control may be hidden by author content. Most teams will not target AAA conformance, but if you are bidding on a government contract that asks for it, this is the one most likely to bite you.

2.4.13 Focus Appearance — Level AAA

The focus indicator has to meet a minimum area and contrast spec. Specifically, the indicator must enclose the focused control with a border thickness of at least 2 CSS pixels, and the indicator color must have a 3:1 contrast ratio against the unfocused state.

I bring this up even though it is AAA because the failure mode is so common. Designers strip the default outline in resets, then add a 1-pixel grey ring that looks fine on a Figma mockup and disappears against a grey button background in production. If you have ever written outline: none and forgotten to put something back, you have shipped this.

/* The wrong way */
button:focus { outline: none; }

/* The right way — always pair with a visible alternative */
button:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Use :focus-visible, not :focus. The former only shows the ring for keyboard users; mouse clicks do not trigger it. This is the modern default and it is what your designers actually want.

2.5.7 Dragging Movements — Level AA

If your interface requires a dragging motion to do something, that something must also be doable with a single pointer down/up — no drag — unless the dragging is essential.

Where this breaks: kanban boards, range sliders without keyboard equivalents, signature pads, map panning, custom date pickers that use drag-to-select. The fix is not to remove the drag, it is to add a non-drag path.

For a kanban card: arrow keys + space to pick up, arrow keys to move, space to drop. The dnd-kit library gets this right out of the box and is what I recommend over react-beautiful-dnd today. For a range slider: arrow keys to nudge the thumb. The native <input type="range"> already does this. For a map: keyboard pan controls, or "+/-" zoom buttons next to the canvas.

The point of this criterion is not "no drag interactions ever." It is "drag cannot be the only way." Users with motor impairments, users on trackpads with bad palm rejection, and users on switch devices all benefit. So do power users who would rather hit a key than wrestle a mouse.

2.5.8 Target Size (Minimum) — Level AA

The target size for pointer inputs must be at least 24 by 24 CSS pixels, unless the target is in a sentence (inline links in body text are exempt), is browser default size, has equivalent functionality available at a larger size, or is essential to the way the information is presented.

24 by 24 is smaller than the 44 by 44 you may have seen from Apple's HIG. WCAG settled on 24 because it is the minimum at which most users can hit a target reliably; 44 is best practice but not the bar.

Where this breaks: icon-only buttons in toolbars, close buttons on toasts, social share icons in footers, sort arrows in tables, "edit" pencils next to list items. The "browser default" exemption covers checkboxes and radios, which are typically smaller than 24 pixels and are exempted. The "inline" exemption covers links inside paragraphs of text.

The simplest fix is invisible padding. A 16-pixel icon with 4 pixels of padding on each side meets the criterion and looks identical to the user.

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  padding: 4px;
}

When two interactive targets sit close to each other, the 24 pixel rule applies to the bounding box of each, not their visual size. A 16-pixel button surrounded by enough padding to push neighbors away can pass.

3.2.6 Consistent Help — Level A

If your site offers help mechanisms — contact info, a help link, a chat widget, an FAQ link, a self-service form — they must appear in the same relative order on every page where they appear. You do not have to offer help, but if you do, you cannot move it around.

Where this breaks: marketing pages that put a "Talk to sales" button in the header, then product pages that put it in the footer, then a support page that puts it as a floating widget. The placement does not have to be identical pixel-wise; it has to be in the same relative order in the DOM relative to other landmarks.

Fix: put help mechanisms in your shared layout — header, footer, or both — and never override them in individual page templates. This is a content-strategy fix more than a code fix.

3.3.7 Redundant Entry — Level A

Information the user previously entered in the same process must be either auto-filled or available for the user to select, except where re-entering it is essential (password confirmation, security verification).

Where this breaks: checkout flows that ask for shipping address, then ask for billing address with no "same as shipping" checkbox. Multi-step forms that drop state when the user navigates back. Account-creation flows that ask for email on step 1 and then again on step 4 for confirmation.

For React forms, this is mostly a state-management problem. If your form fields are uncontrolled and you remount them on step changes, you will fail this. Lift the state up, or use a form library that persists across step transitions (React Hook Form's FormProvider does this cleanly).

// The "same as shipping" pattern
<Checkbox
  checked={billingMatchesShipping}
  onCheckedChange={(checked) => {
    setBillingMatchesShipping(checked)
    if (checked) {
      setBillingAddress(shippingAddress)
    }
  }}
>
  Billing address is the same as shipping
</Checkbox>

3.3.8 Accessible Authentication (Minimum) — Level AA

A cognitive function test (remembering a username, solving a puzzle, transcribing characters from a CAPTCHA) is not required for any step in an authentication process, unless that step provides an alternative, the test is to identify objects or non-text content that the user provided, or the test recognizes the user.

In practice this means: a) password fields must support paste from a password manager, b) you cannot block a user from authenticating just because they cannot pass a CAPTCHA, c) email magic links and OAuth are acceptable alternatives.

The single most common failure I see is onpaste="return false" on password fields. That one line of code, often added by a well-intentioned developer who thought it improved security, locks out every user who relies on a password manager. Remove it.

<!-- Wrong. Remove this. -->
<input type="password" onpaste="return false" />

<!-- Right. Let the password manager do its job. -->
<input
  type="password"
  autocomplete="current-password"
/>

CAPTCHAs need an alternative path. Cloudflare Turnstile and hCaptcha both ship accessible alternatives; turn them on. If you are still using a custom "type the wavy letters" CAPTCHA, you are failing this criterion and probably driving away real users at the same time.

3.3.9 Accessible Authentication (Enhanced) — Level AAA

Same idea, no exception for object recognition. If you are targeting AAA, audit every step of every auth flow and make sure none of them depend on memorization, transcription, or recall.

What got removed: 4.1.1 Parsing

WCAG 2.2 removed 4.1.1, which required that markup have complete start and end tags, be nested per spec, contain no duplicate attributes, and have unique IDs. The rationale: modern browsers recover from these errors so reliably that the criterion no longer correlated with real user harm. Screen readers in 2026 do not care if your <div> is missing a closing tag.

If your last accessibility audit flagged you for parsing issues, you may now ignore them — but you should still fix duplicate IDs, because they break <label for="..."> and aria-labelledby, which are still required by other criteria.

How inklu maps to all of this

inklu scans against WCAG 2.2 by default. Our axe-core foundation (v4.11) catches the criteria that have a clean automated signature — focus appearance, target size, redundant entry where it manifests as a missing autocomplete attribute. Our 50+ proprietary rules on top fill the gap on the criteria that require more semantic understanding, like Consistent Help (which depends on comparing structure across pages) and Focus Not Obscured (which depends on knowing what is sticky in your layout).

When a violation is detected, we generate a fix with GPT-4o and deliver it as a GitHub pull request — never a commit to your default branch. Two thirds of our customer scans run on JSX/TSX codebases, which is where the new 2.2 criteria show up most. See common WCAG violations in React/JSX for a closer look at what trips up React apps specifically.

If your team is preparing for AODA, ADA Title II, or EAA conformance — all three of which cite WCAG 2.2 as the bar — book a demo at inklu.io or email hello@inklu.io. We will scan a page or a repo, walk you through what we found, and show you what the PRs look like before you decide anything.