Run an accessibility checker extension. A second later, violations appear: contrast failures, missing labels, broken ARIA.
It feels instant. Almost automatic.
But there’s a specific technical process behind every result. Understanding how in-browser accessibility testing works makes you a more effective developer; you’ll know exactly what the tool is checking, why certain issues surface and others don’t, and where you need to step in manually.
Here’s the full picture, from click to WCAG violation.
How Chrome extensions access your live page
An accessibility checker doesn’t analyse your HTML as a static file. It runs live, inside Chrome, against the fully rendered page; after JavaScript has executed, after CSS has resolved, after every third-party script has done its thing.
The mechanism is a content script: JavaScript the extension injects directly into your active tab when you trigger a scan. It has full access to the document object- the same DOM your own code works with.
<!-- What's in your source file -->
<button id="submit">Submit</button>
<!-- What's in the DOM when the extension actually runs -->
<button id="submit" aria-disabled="true" tabindex="-1">Submit</button>
A static linter reads the first version. The extension reads the second. These can produce genuinely different results, and the live rendered version is generally closer to what users encounter, though some issues also depend on interaction timing, AT mode, and virtual cursor behaviour that only manual testing with real assistive technology can surface.
This is the core advantage of accessibility testing in Chrome over markup-level scanning and why browser-based testing catches failures that source-code analysis misses.
The DOM vs. The Accessibility Tree: What the extension actually reads

Having access to the DOM is just the starting point. To actually test accessibility, the extension needs to approximate parts of what assistive technologies receive, not literally experience the page as JAWS or VoiceOver does, but evaluate the same structural signals those tools rely on.
When Chrome renders a page, it builds two parallel structures. The DOM contains everything. The accessibility tree is a filtered, computed version of that structure, the one assistive technologies actually navigate. A DOM accessibility checker that only reads markup without rendering it misses everything that happens in between.
These two structures aren’t the same, and the differences matter:
| What you write | What happens in the accessibility tree |
|---|---|
<button>, <nav>, <h1> | Implicit roles assigned automatically |
aria-hidden="true" | Typically removes element and all children from Chrome’s accessibility tree, referenced hidden content can still contribute to accessible names via aria-labelledby / aria-describedby per the AccName spec; cross-AT behavior varies |
display: none or visibility: hidden | Element typically excluded (Chrome behavior; verify across your target ATs) |
.sr-only CSS (clip + position) | Typically stays in the tree and is announced — depends on the clipping implementation being correct |
aria-label="Close" | Overrides whatever name was computed |
The behaviours above reflect general browser rules, but exact AT exposure can vary across browser and assistive technology combinations. The Chrome DevTools Accessibility panel reflects Chrome’s implementation specifically, not a universal source of truth. Cross-browser AT testing remains the only reliable way to confirm how content is exposed in your target environments.
A <div> with no ARIA has no meaningful role in the tree. A button with aria-hidden="true" is typically removed from Chrome’s accessibility tree, even if it’s visible on screen. Content visually hidden with a .sr-only clipping utility is intended to stay in the tree and be announced by screen readers, but this depends on the implementation being correct. Accessibility checkers read geometry as well as computed styles, and certain clipping implementations can produce inconsistent results between tools and occasionally generate unexpected findings.
One more thing, the accessibility tree doesn’t capture consistently: CSS-generated content. Pseudo-elements like ::before and ::after may appear visually on screen while not contributing meaningful accessible text reliably across browsers and assistive technologies. Teams using CSS-generated labels or icon characters should not rely on that content reaching screen reader users.
Note: opacity: 0 alone does not remove an element from the accessibility tree or sequential focus order. The element still occupies its layout space, invisible interactive elements can still receive keyboard focus and be activated, meaning a screen reader user can tab to an invisible button without knowing it’s there. The actual interaction and focus behaviour also depends on tabindex, pointer-events, and any author-defined event handling. Some assistive technologies may additionally expose or suppress such elements differently from what Chrome’s accessibility tree suggests, so testing with your target ATs is the only way to confirm real-world behaviour.
Opacity alone doesn’t remove an element from anything. To control how an element is exposed:
tabindex="-1"removes it from the sequential Tab key flow but screen reader users can often still discover it through virtual navigation, depending on their reading mode, navigation mode, and the specific AT/browser combination. It is not reliably hidden from assistive technology, only from keyboard tabbing order.aria-hidden="true",visibility: hidden, ordisplay: noneremoves the element from Chrome’s accessibility tree in most cases, though exact screen reader exposure can still vary across AT and browser combinations.
For content that should be announced by screen readers but hidden visually, skip-link labels, off-screen descriptions a correctly implemented .sr-only CSS clipping utility is the right approach. When implemented correctly, it keeps content in the tree and reachable; verify the behavior with your target ATs.
You can inspect Chrome’s accessibility tree yourself: open DevTools, select any element, and look at the Accessibility tab in the right panel. These values closely reflect the information accessibility engines use during evaluation; alongside DOM structure, computed styles, ARIA state, and geometry calculations.
How ARIA roles and attributes are validated

ARIA is where a lot of developers assume things are fine when they aren’t. The issues are invisible in visual review but create real barriers for screen reader users.
The WAI-ARIA specification defines a formal role taxonomy, widget roles, landmark roles, document structure roles, each with rules about where it can be used and what it requires. Accessible names are computed using the Accessible Name and Description Computation (AccName) algorithm. When Chrome extensions detect WCAG issues related to ARIA, they’re validating against these specs.
Is the role valid on this element?
<!-- Valid: <div> accepts any role -->
<div role="button" tabindex="0">Submit</div>
<!-- Invalid: abstract roles cannot be used in HTML content -->
<div role="widget">Submit</div>
<!-- Many native HTML elements restrict which ARIA roles are allowed.
Incompatible overrides like this may be ignored or flagged by validators. -->
<img role="tablist" src="navigation.png" alt="Tab navigation background" />
Are required ARIA properties present?
Some roles only make sense with specific attributes. Without them, assistive technology can’t determine what state the element is in.
<!-- Violation: role="checkbox" requires aria-checked -->
<div role="checkbox">Accept terms</div>
<!-- Valid -->
<div role="checkbox" aria-checked="false">Accept terms</div>
Are unsupported attributes being applied?
Some ARIA attributes simply don’t apply to certain roles. The spec doesn’t list them as supported, and applying them creates a state that validators flag and assistive technologies may ignore or misinterpret.
<!-- Violation: aria-checked is not applicable to role="link" -->
<a href="/signup" aria-checked="true">Sign up</a>
<!-- Violation: aria-modal is only valid on role="dialog" and role="alertdialog" -->
<!-- aria-modal="true" signals to AT that content outside the element is inert
and unavailable — but this is not a magic tree-hiding operation. Authors
must still implement focus trapping and interaction blocking explicitly.
Applied to a <button>, it creates invalid semantics that some ATs may
interpret unexpectedly; others will simply ignore it. -->
<button aria-modal="true">Submit</button>
Validation also covers implicit roles. Every HTML element has a default <button> is implicitly role="button", <nav> is implicitly role="navigation". These are checked against the same rules even when you haven’t written a single ARIA attribute.
One pattern that trips up a lot of developers: the difference between disabled and aria-disabled="true". The native disabled attribute removes the element from interaction entirely and excludes it from focus order. aria-disabled="true" only exposes the disabled state to assistive technology, the element stays interactive and focusable unless you separately manage its behavior. Using them interchangeably produces different outcomes depending on the assistive technology and how focus management is handled.
How accessible names are computed
Once ARIA is validated, the extension checks something closely related: the accessible name the text a screen reader announces when a user reaches an element. Missing accessible names are one of the most common WCAG failures, and they’re almost always invisible in visual review.
Names can come from several places. The order below is a common simplified model; the actual AccName algorithm varies by role and element type, and some roles prohibit certain naming mechanisms entirely. Use this as a useful mental model, not a universal rule:
| Priority | Source | How it works |
|---|---|---|
| 1 | aria-labelledby | Reads text from the referenced elements. Fails silently if the referenced ID doesn’t exist — name becomes empty. |
| 2 | aria-label | Uses the attribute value directly. |
| 3 | Native label | <label for="id">, alt text on images, <caption> on tables. |
| 4 | title attribute | Conditional fallback only — applies when higher-priority sources are absent, varies by element type, and is not a reliable primary naming approach. |
| 5 | Text content | For buttons, links, and headings. |
| 6 | Nothing | Violation. |
This is why a button with only an icon image fails silently:
<!-- No accessible name — violation -->
<button><img src="icon-send.svg" /></button>
<!-- Fixed: alt text on the image provides the name -->
<button><img src="icon-send.svg" alt="Send message" /></button>
<!-- Also valid: aria-label directly on the button -->
<button aria-label="Send message"><img src="icon-send.svg" alt="" /></button>
The extension runs this computation for every interactive element and image and checks whether the result is non-empty. For a deeper look at how this plays out specifically with images, see how to check if an image has alt text.
What the tool can’t tell you is whether the name is a good description. That still needs a human.
How colour contrast is calculated in the browser
Contrast checking works differently from ARIA and accessible name validation. Instead of checking structure or attributes, the extension computes the colours the browser actually renders, which means reading resolved CSS, not authored values.
That distinction matters most for runtime changes. Source-level tools often cannot determine runtime-resolved values, particularly when CSS variables change dynamically, based on user interaction, theme switching, or JavaScript state. An accessibility checker running in Chrome calls window.getComputedStyle(element).color and gets the actual rendered value at that moment, regardless of how it was set.
Background colour is where this method has real limitations. getComputedStyle returns the declared CSS background-color of that specific element, but when a background is complex (a gradient, a background image, a pseudo-element, or a layered overlay), the checker cannot reliably determine the effective painted background. The property may return rgba(0, 0, 0, 0) or another non-representative value in those cases, which is why confident contrast calculation isn’t always possible. Different engines apply additional heuristics beyond a single lookup, but the fundamental constraint applies regardless.
Rule engines like axe-core do walk up the DOM ancestors to find a non-transparent background, which handles simple inherited solid colours reasonably well. But when they encounter a gradient, a background image, or complex CSS composition, they cannot reliably compute pixel-level compositing.
Rather than produce an inaccurate result, they emit a “Needs Review” flag rather than a hard fail, which is the correct response to genuine ambiguity. Treating contrast on gradient- or image-backed text as requiring manual evaluation is the right call.
The practical implication: contrast results on pages with gradient backgrounds, image-backed sections, or CSS-heavy design systems should be treated as incomplete, not authoritative. Those elements need manual contrast evaluation.
For the cases where a solid, inherited background colour is resolvable, the extension computes the relative luminance (Y) of both colours. Relative luminance isn’t a simple normalisation of raw RGB values — it requires a non-linear sRGB transformation (gamma expansion) applied to each colour channel before weighting:
Y = 0.2126 × R_linear + 0.7152 × G_linear + 0.0722 × B_linear
Each channel is first gamma-expanded from its sRGB value. A naive channel / 255 division produces incorrect results because it ignores this non-linearity. Y ranges from 0 (absolute black) to 1 (absolute white). The contrast ratio is then:
Contrast ratio = (Y_lighter + 0.05) / (Y_darker + 0.05)
That ratio is checked against WCAG thresholds:
| Text type | WCAG AA (SC 1.4.3) | WCAG AAA (SC 1.4.6) |
|---|---|---|
| Normal text | 4.5 : 1 | 7 : 1 |
| Large text (≥ 18pt / ≥ 14pt bold per spec; ≈ 24px / 18.66px at standard DPI — rem scaling may differ) | 3 : 1 | 4.5 : 1 |
| Non-text UI components and graphics (SC 1.4.11 — a separate criterion from text contrast) | 3 : 1 | not defined |
If the ratio falls short, the element is flagged with the actual number alongside the requirement, so you know exactly how far off it is, not just that it failed.
How the rule engine runs all these checks at once
ARIA validation, accessible name computation, and contrast calculation all run as part of a coordinated rule engine that scans every element on the page in a single pass.
Each rule follows the same flow: query the DOM for relevant elements, filter out ones that don’t apply; hidden elements, elements inside aria-hidden subtrees run the check and record the result.
Results are categorised by severity (critical, serious, moderate, minor) and mapped to the specific WCAG success criteria they correspond to. The extension then highlights affected elements directly on the page, so you have context alongside each violation rather than an abstract list.
What automated accessibility checks can and can’t catch
| Check | Automated? | Why |
|---|---|---|
| alt attribute is present | Yes | Binary DOM check |
| Alt text is meaningful | No | Requires understanding the image in context |
| Contrast of static text | Yes | Computed from resolved styles |
| Contrast behind images or gradients | Partial | Most engines flag for manual review |
| ARIA role is valid | Yes | Validated against the role taxonomy |
| ARIA state is correct at runtime | Partial | Reads current state; can’t simulate interactions |
| Heading hierarchy consistency | Partial | Tools detect skipped levels or unusual structures, but whether they constitute a violation depends on context |
| Heading text is useful | No | Requires judgment |
| Form input has a label | Yes | Accessible name computation returns empty |
| Form error messages are helpful | No | Requires judgment |
| Focus moves correctly after interactions | No | Requires triggering interactions and observing |
| Screen reader experience is correct | No | Requires assistive technology testing |
The right approach: use automated checks to clear the rule-based failures fast. Then spend manual testing time on what actually needs a human: complex interactions, ARIA logic edge cases, real screen reader behaviour with JAWS, NVDA, or VoiceOver.
If you’re unsure when a Chrome extension is enough and when to go further, that’s a useful place to start. And if you want to build these checks into your team’s daily workflow, there’s a practical guide for that too.
Run these checks instantly right inside your browser
The WebYes Accessibility Checker runs a full WCAG 2.0, 2.1, and 2.2 scan: ARIA validation, accessible name computation, contrast calculation, heading structure, sequential focusability anomalies (such as explicit tabindex traps), live in your browser, against the fully rendered page. No setup. No sign-in.
Install it, open any page, and see exactly what’s there.