Automated A11y Checks in Cypress and Playwright
Automated accessibility testing with axe-core covers only about 30 to 40 percent of all WCAG criteria, but those categories, things like color contrast, missing alt text, and ARIA errors, can be reliably integrated into any Cypress or Playwright pipeline. This article shows how to configure thresholds, block CI builds only on new violations, and catch typical Magento- and Hyvä-specific barriers early.
Table of Contents
- 1. Why automated accessibility testing is essential in the E2E workflow
- 2. What axe-core detects automatically, and where manual review is still required
- 3. Cypress integration with cypress-axe
- 4. Playwright integration with @axe-core/playwright
- 5. Severity thresholds and rule configuration
- 6. CI pipeline: blocking only new violations, not legacy debt
- 7. Magento- and Hyvä-specific accessibility issues
- 8. Complementing manual tests: keyboard, screen reader, reading order
- 9. Testing methods compared
- 10. Summary
- 11. FAQ
1. Why automated accessibility testing is essential in the E2E workflow
Accessibility is no longer optional: with the German Accessibility Strengthening Act (BFSG) and the European Accessibility Act, digital stores in the EU have been legally required to offer accessible online services since 2025. Manual audits by external testing bodies are expensive and usually happen only shortly before a release, when fixes are already costly. Teams that instead integrate accessibility checks directly into their existing E2E test suite catch regressions just as early as functional bugs, namely on every pull request.
axe-core is the open-source engine built by Deque Systems that also underpins many other tools like Lighthouse and WAVE. As an npm library, it plugs directly into Cypress or Playwright and inspects the actual rendered DOM at runtime, not just static HTML. That distinction matters for modern frontends: an Alpine.js dropdown or a dynamically loaded form often looks different in the source code than in the final DOM, and only a runtime check captures the actual state a user experiences.
2. What axe-core detects automatically, and where manual review is still required
According to research by Deque, roughly 30 to 40 percent of all WCAG success criteria can realistically be checked automatically and reliably. These include insufficient color contrast (WCAG 1.4.3), missing alt attributes on images (1.1.1), form fields without an associated label (1.3.1 and 4.1.2), broken or duplicate ARIA roles and attributes, an illogical heading hierarchy, and a missing or empty <title>. These categories are programmatically testable because they are structural properties that can be unambiguously detected in the DOM.
What automated tools can't detect are aspects that require human judgment: whether the tab order during keyboard navigation logically follows the visual flow, whether a screen reader actually reads a page in an understandable way, whether an existing alt text meaningfully describes the image content instead of merely existing as a formality, or whether focus management inside a modal dialog works correctly. Automated tests are a necessary but not a sufficient complement to planned manual reviews here.
3. Cypress integration with cypress-axe
The cypress-axe package wraps axe-core as a Cypress command and fits seamlessly into an existing E2E suite. After every cy.visit(), you need to call cy.injectAxe() to inject the axe-core runtime into the page before cy.checkA11y() runs the actual check. It's important to run checks not just once per page but after every relevant interaction, such as opening a dropdown, a form validation, or a modal, since those dynamic states are exactly where most violations turn up.
A second important point: if the DOM gets fully re-rendered by a framework like Alpine.js, cy.injectAxe() must be called again, since the previous injection would otherwise be lost. The optional fourth parameter of cy.checkA11y() controls whether detected violations fail the test immediately or are just logged to the console first, which is especially useful during the rollout phase.
// cypress/e2e/accessibility/checkout.cy.js
describe('Checkout accessibility', () => {
beforeEach(() => {
cy.visit('/checkout');
cy.injectAxe(); // inject axe-core into the page after each navigation
});
it('has no serious or critical violations on the shipping step', () => {
cy.checkA11y('#checkout-container', {
runOnly: ['wcag2a', 'wcag2aa', 'wcag21aa'],
}, null, true); // skipFailures=true, log first before enforcing hard failures
});
it('has no violations after opening the payment method dropdown', () => {
cy.get('[data-test="payment-method-toggle"]').click();
cy.injectAxe(); // re-inject if Alpine.js replaced the surrounding DOM
cy.checkA11y('[data-test="payment-methods"]');
});
});
4. Playwright integration with @axe-core/playwright
For Playwright, @axe-core/playwright provides a fluent API called AxeBuilder that attaches directly to an open page instance. After page.goto(), new AxeBuilder({ page }).analyze() is enough to trigger a complete check. Since Playwright drives real browser engines, Chromium, Firefox, and WebKit, accessibility checks can additionally run cross-browser, which matters because ARIA support and focus behavior can genuinely differ between engines.
withTags() filters the WCAG categories being checked, exclude() carves out uncontrollable third-party code like chat widgets, and include() limits the check to a specific container. The full results can be embedded directly into the Playwright HTML report via testInfo.attach(), so reviewers can see violations, including the affected selector and help URL, without a separate tool.
// tests/accessibility/product-page.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('product page has no critical accessibility violations', async ({ page }) => {
await page.goto('/catalog/product/view/id/123');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('#third-party-chat-widget') // exclude uncontrolled third-party markup
.analyze();
// Attach the full report to the Playwright HTML report for review
await test.info().attach('axe-results', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
const critical = results.violations.filter(v => v.impact === 'critical' || v.impact === 'serious');
expect(critical, `Found ${critical.length} serious/critical violations`).toHaveLength(0);
});
5. Severity thresholds and rule configuration
Every axe-core violation carries an impact level: minor, moderate, serious, or critical. Not every team wants to block the build over a minor contrast error on a rarely visited subpage. The rules configuration lets you enable or disable individual rules, while runOnly with tags like wcag2a, wcag2aa, wcag21aa, or best-practice lets you tailor the rule set precisely to your own compliance goals.
A pragmatic compromise that has proven itself in practice: the CI build only fails on serious- and critical-level violations, while moderate and minor ones are logged and collected in a dashboard without blocking the build. That preserves team velocity without overlooking the most serious barriers.
{
"rules": {
"color-contrast": { "enabled": true },
"landmark-one-main": { "enabled": false },
"region": { "enabled": false }
},
"runOnly": {
"type": "tag",
"values": ["wcag2a", "wcag2aa", "wcag21aa", "best-practice"]
},
"resultTypes": ["violations"],
"reporter": "v2"
}
6. CI pipeline: blocking only new violations, not legacy debt
The biggest obstacle to introducing automated accessibility testing: mature applications often carry hundreds of existing violations. An immediate hard gate check would break every build and would more likely tempt the team to disable the check entirely rather than take it seriously. The proven approach instead is a baseline snapshot: the current state of violations gets committed to the repository as a JSON file, and CI compares only the diff against that baseline on every build.
New violations not present in the baseline block the merge immediately. Already-known legacy violations are tracked separately and prioritized in their own tickets, without disrupting the day-to-day development flow. Each rule ID and affected selector serves as a fingerprint for the comparison, so individual fixes can also be removed from the baseline once they're resolved.
# .github/workflows/accessibility.yml
name: Accessibility Regression Check
on: [pull_request]
jobs:
axe-baseline-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Run axe-core against staging pages
run: npm run test:a11y -- --json > current-violations.json
- name: Compare against committed baseline
run: |
node scripts/diff-a11y-baseline.js \
--baseline .a11y-baseline.json \
--current current-violations.json \
--fail-on-new
- name: Upload violation report
if: failure()
uses: actions/upload-artifact@v4
with:
name: accessibility-report
path: current-violations.json
7. Magento- and Hyvä-specific accessibility issues
In Hyvä stores, axe-core tends to surface recurring patterns: insufficient color contrast from Tailwind utility classes like text-gray-400 on a white background, which often falls below the required 4.5:1 contrast ratio. Just as common: icon-only buttons such as the mini-cart icon, the search toggle, or the wishlist heart, without an aria-label. Sighted users can infer the function from the icon, but for screen reader users a button without an accessible label remains a meaningless "button, unlabeled".
An Alpine.js-specific issue involves x-show: the directive only hides elements via CSS display: none, while the content, if timing is misconfigured or during transitions, can remain present in the accessibility tree and sometimes even focusable. x-cloak combined with x-if, which removes elements from the DOM entirely instead of just hiding them, reliably avoids this problem. In checkout, form fields without a <label for> association are also a perennial issue, especially with custom-styled select components where a plain placeholder is mistakenly treated as sufficient.
<!-- Before: icon-only button without aria-label, x-show leaves content in the a11y tree -->
<button @click="miniCartOpen = !miniCartOpen">
<svg class="w-6 h-6" ...></svg>
</button>
<div x-show="miniCartOpen" class="absolute right-0 ...">
...
</div>
<!-- After: accessible label, x-cloak avoids the flash, x-if removes hidden content from the DOM -->
<button
@click="miniCartOpen = !miniCartOpen"
:aria-expanded="miniCartOpen.toString()"
aria-label="{{ __('Open mini cart') }}"
>
<svg class="w-6 h-6" aria-hidden="true" ...></svg>
</button>
<template x-if="miniCartOpen">
<div x-cloak class="absolute right-0 ..." role="dialog" aria-label="{{ __('Mini cart') }}">
...
</div>
</template>
<!-- Checkout input without an associated label triggers axe-core's "label" rule -->
<input type="text" id="street_1" name="street[0]" placeholder="{{ __('Street Address') }}">
<!-- Fix: explicit label, a placeholder alone is not an accessible name -->
<label for="street_1" class="sr-only">{{ __('Street Address') }}</label>
<input type="text" id="street_1" name="street[0]">
8. Complementing manual tests: keyboard, screen reader, reading order
A keyboard-only pass is one of the cheapest and most effective manual tests: walk through the entire checkout flow using only Tab, Shift+Tab, Enter, and Escape. Every interactive element must be reachable and receive a visible focus ring, no focus trap should keep the user stuck inside a modal, and the tab order must match the page's visual flow.
A screen reader spot check with NVDA on Windows or VoiceOver on macOS and iOS on critical pages like the product detail page and checkout reveals whether live announcements, for example on cart updates (aria-live), are actually read aloud and whether the reading order matches the visual layout, something axe-core simply cannot evaluate by design. These checks can't be fully automated, but they should be a fixed part of sprint reviews or scheduled as a quarterly audit, rather than only being addressed reactively when complaints come in.
9. Testing methods compared
Automated and manual methods cover different areas and complement rather than replace each other. The table below shows what each method is best suited for and where its limits lie.
| Testing method | Automatable | Typical shortcoming | Recommended use |
|---|---|---|---|
| axe-core (Cypress/Playwright) | Yes, fully automated | Detects only about 30-40% of WCAG criteria | On every pull request / build |
| Lighthouse Accessibility Audit | Partially automated | Only a single-page snapshot, no interaction states | Quick spot checks alongside axe-core |
| Manual keyboard test | No, manual | Time-consuming, not CI-suitable | Before every major release |
| Screen reader test (NVDA/VoiceOver) | No, manual | Requires trained testers | Quarterly on critical flows |
| Visual focus review | No, manual | Easily overlooked with custom components | For every new UI pattern |
| Contrast checker browser plugin | Partially automated | Only checks the visible viewport | Complements the axe-core contrast rule |
In practice, automated and manual methods complement each other: axe-core catches structural errors on every commit, while scheduled manual audits cover the remaining 60 to 70 percent of WCAG criteria that require context and human judgment and simply cannot be captured in a CI pipeline.
Mironsoft
Accessibility testing, axe-core integration, and Hyvä a11y optimization for Magento stores
Ready to roll out automated accessibility testing professionally?
We integrate axe-core into your Cypress or Playwright suite, configure sensible thresholds, and set up a baseline that blocks new violations without slowing the build down with legacy debt.
axe-core CI integration
Cypress or Playwright setup with sensible severity thresholds
Magento/Hyvä a11y audit
Targeted checks of color contrast, icon buttons, and Alpine.js states
Monitoring & baseline setup
Baseline diffing in the CI/CD pipeline without legacy debt blocking the build
10. Summary
Accessibility testing with axe-core solves a clearly scoped but important part of the problem: it reliably catches structural WCAG violations like missing color contrast, missing labels, and ARIA errors on every commit, instead of discovering them shortly before a release or, worse, only through user complaints. Integration via cypress-axe or @axe-core/playwright takes just a few lines of code, but it only delivers real value once you pair it with sensibly configured severity thresholds and a baseline strategy that blocks new violations without stumbling over existing legacy debt.
Managing expectations realistically is key: axe-core covers roughly 30 to 40 percent of WCAG criteria, and the rest, things like keyboard navigation, screen reader comprehensibility, and a sensible reading order, remains the job of scheduled manual audits. In Magento and Hyvä stores, automated checks most often run into Tailwind contrast errors, missing labels on icon buttons, and Alpine.js states that stay in the accessibility tree despite being visually hidden.
Accessibility Testing with axe-core - The Essentials at a Glance
Assess coverage realistically
axe-core automatically finds about 30-40% of WCAG criteria; the rest needs manual review.
CI without legacy blockers
Baseline diffing blocks only new violations; known legacy issues get resolved separately.
Magento/Hyvä pitfalls
Specifically check Tailwind contrast, icon buttons without aria-label, and x-show states.
Schedule manual tests
Anchor keyboard and screen reader checks firmly in sprint reviews or quarterly audits.