How to Test Web Accessibility with Cypress
Add repeatable accessibility scans to Cypress with cypress-axe, cover meaningful UI states, and pair automated checks with assertions and manual testing.
Cypress accessibility testing adds checks to the end-to-end and component tests you already run. For a local, open-source scan in a test, use the community cypress-axe plugin: inject axe-core, reach a meaningful page or component state, and call cy.checkA11y(). Then add Cypress assertions for product-specific names and behavior, and manually evaluate experiences a scanner cannot judge. A clean automated scan is useful evidence, but it does not prove accessibility or WCAG conformance. Cypress’s accessibility testing guide describes these complementary approaches.
1. Choose how Cypress will check accessibility
There are three useful layers. They solve different problems and can be combined:
| Approach | Where checks run | Good fit | Tradeoff |
|---|---|---|---|
cypress-axe |
In your Cypress test, against the rendered DOM | Open-source scans with explicit test-level control | Scans add work to test execution; you decide which states to scan. |
| Cypress Accessibility | In Cypress Cloud, processing recorded test data | Teams using Cypress Cloud that want reported checks without adding scan commands to test code | It is a paid Cypress Cloud option; review current plan terms with Cypress. |
| Regular Cypress assertions | In test code | Checking requirements specific to your application, such as the expected button name or keyboard outcome | You must know and assert the expected behavior. |
The rest of this walkthrough uses cypress-axe. Cypress Cloud Accessibility is an alternative, not a replacement for application-specific assertions or human evaluation. Cypress says automation can catch up to 57% of issues that would appear in a manual audit; the figure is not a guarantee for any particular project and does not mean a passing scan establishes conformance. See Cypress’s accessibility automation principles.
2. Install and register cypress-axe
Install the plugin as a development dependency in the project that already has Cypress configured:
npm install --save-dev cypress-axe
Register its commands in the support file Cypress loads for your tests. In a typical current project, that file is cypress/support/e2e.js for end-to-end tests. If your project uses TypeScript, use the corresponding .ts support file.
// cypress/support/e2e.js
import 'cypress-axe'
For component tests, register the import in the component support file configured by your Cypress project, often cypress/support/component.js. Follow the support-file path in your project’s Cypress configuration if it differs. The plugin adds cy.injectAxe() and cy.checkA11y(). Cypress documents the plugin integration and scan behavior in its accessibility guide; see also the cypress-axe package documentation for options available in your installed release.
3. Scan a real page state
Inject axe after the application has loaded, then scan. This complete example visits a page and checks the current state:
// cypress/e2e/accessibility.cy.js
describe('home page accessibility', () => {
it('has no detected axe violations on initial load', () => {
cy.visit('/')
cy.injectAxe()
cy.checkA11y()
})
})
cy.checkA11y() checks the state that exists when the command runs. A page-load scan will not automatically inspect a dialog that opens later, a validation error, or an expanded menu. Write tests that reach those states and scan them separately. Use your application’s normal test setup for authentication, seeded data, and base URLs so the state is repeatable.
Scan after important interactions
describe('sign-in accessibility states', () => {
beforeEach(() => {
cy.visit('/sign-in')
cy.injectAxe()
})
it('scans the initial form', () => {
cy.checkA11y()
})
it('scans the validation-error state', () => {
cy.get('form').submit()
cy.contains('Email is required').should('be.visible')
cy.checkA11y()
})
it('scans the open help dialog', () => {
cy.contains('button', 'Help').click()
cy.get('[role="dialog"]').should('be.visible')
cy.checkA11y('[role="dialog"]')
})
})
Replace selectors and visible messages with your application’s actual markup. The optional selector scopes a check to part of the DOM; it can help focus a component check, but a scoped scan cannot report issues outside that region. Keep at least some page-level checks for rules that depend on the entire document.
4. Configure rule scope intentionally
By default, cy.checkA11y() uses axe-core’s standard rules. The plugin lets you scope scans to a context and configure rules or tags when your project needs a deliberate subset. For example, this scopes the check to the main content and disables one rule for this call:
cy.checkA11y('main', {
rules: {
'some-rule-id': { enabled: false }
}
})
Use a real rule ID and confirm the configuration shape against the installed cypress-axe documentation before adopting custom configuration. Avoid disabling a rule simply to make a build green. Record why a finding is inapplicable, keep it visible for review where possible, and revisit exclusions as the interface changes. Different tools and configurations cover different sets of rules.
Do not assume Cypress Cloud defaults equal all WCAG checks
Cypress Accessibility has its own defaults and configuration, separate from a local cypress-axe scan. Its default rules include axe-core WCAG 2.0 and 2.1 Level A/AA rules and Deque Best Practices recommendations. WCAG 2.2, AAA, experimental, and deprecated groups are off unless enabled. Cypress also lists color-contrast, no-autoplay-audio, and meta-refresh as off by default in that product. Best Practices are recommendations, not WCAG success criteria. Check the Cypress Accessibility FAQ for current product behavior and configuration. Do not apply these Cypress Cloud defaults to a local axe run without checking its actual configuration.
5. Add assertions for what matters to your users
A scanner cannot know whether a particular control has the right name for your product, whether a workflow is understandable, or whether keyboard interaction achieves the intended task. Add ordinary Cypress assertions for critical behavior, such as labels, names, state changes, and keyboard operation.
it('has a named submit button and announces the result', () => {
cy.visit('/contact')
cy.get('button[type="submit"]')
.should('have.text', 'Send message')
cy.get('input[name="email"]')
.should('have.attr', 'aria-label', 'Email address')
cy.get('button[type="submit"]').click()
cy.get('[role="status"]').should('contain.text', 'Message sent')
})
Write assertions that match the interface’s semantic requirements. Prefer checking the accessible behavior or user-visible outcome over asserting incidental implementation details. Cypress documents keyboard events through cy.press(); use it where a test needs to verify keyboard interaction, and include manual keyboard checks for real focus order and usability. A locator that finds an element by role or label does not, by itself, establish that the whole interface is accessible.
6. Review findings and keep manual evaluation
- Run the failing test and inspect the rule ID, impact, affected element, and surrounding DOM.
- Fix the underlying markup or interaction rather than hiding a symptom when possible.
- Rerun the relevant states and broader suite to catch regressions.
- Manually test keyboard-only use, focus visibility and order, zoom/reflow, content comprehension, and assistive-technology behavior appropriate to the product.
- Track exclusions and unresolved findings with an owner and reason; do not treat a zero-violation report as a conformance statement.
Some results need human review. Dynamic content, canvas, custom widgets, embedded content, and complex application flows can require context that a rule engine does not have. Conversely, an apparent false positive may reveal that a pattern works in one tested browser or screen reader but has poor support elsewhere. Cypress’s guidance emphasizes understanding a scanner’s coverage and filling gaps with manual evaluation and app-specific assertions.
7. Run accessibility checks efficiently and reliably
- Scan meaningful states, not every duplicate. Cover each important shared component or workflow state at least once. Repeatedly scanning an identical state adds runtime without much new information.
- Do not scan only the initial page. Include menus, modals, form errors, loading/empty states, and other states that introduce distinct UI.
- Watch suite duration. Each in-test scan evaluates applicable rules against DOM elements. As the number of unique states grows, scan time can become material. Cypress recommends managing redundant scans and targeting checks deliberately. See its performance guidance.
- Keep test data deterministic. Stable content, predictable network responses, and explicit waits for the state under test reduce false failures and ensure the scan sees the intended UI.
- Use component tests for shared UI. A component test can centralize checks for a reusable control; retain end-to-end scans where assembled page context or a user flow creates a different state.
- Pin and review dependencies according to your project policy. New axe-core rules can reveal existing issues. The exact versions and compatibility matrix depend on the packages installed; consult their current documentation rather than assuming a universal version pairing.
There is no per-scan charge for the open-source plugin itself described here, but Cypress execution and CI resources have costs under your own infrastructure or plan. Cypress Cloud Accessibility is a paid option; check current Cypress terms for its pricing and limits. A scan’s cost in engineering time also includes diagnosing findings and maintaining meaningful coverage.
8. Troubleshooting common problems
| Symptom | Likely cause | Fix |
|---|---|---|
cy.injectAxe is not a function |
The plugin support import did not run, or it was added to a support file not used by this test type. | Import cypress-axe in the configured end-to-end or component support file, then confirm that Cypress loads that file. |
axe is not defined or scan cannot run |
cy.injectAxe() was omitted, or injection ran before navigation replaced the document. |
Visit or mount the application first, then inject axe and call checkA11y(). Inject again after a full navigation if needed. |
| The scan passes but a dialog/menu has problems | The test scanned the initial state only, before opening the interactive UI. | Exercise the interaction, assert the new state is visible, then scan that state or scope. |
| Results vary between runs | Content or timing is nondeterministic, or asynchronous UI has not settled. | Wait for a meaningful visible condition, stabilize test data and network behavior, and avoid arbitrary sleeps where a state assertion is available. |
| A violation appears to be a false positive | The rule may require context, or the technique may have inconsistent support across assistive technologies. | Inspect the element and rule guidance, reproduce with relevant browser/assistive-technology combinations, and document a narrowly scoped exception only when justified. |
| Scans make the test suite slow | Many repeated scans or large, complex DOM states are being checked. | Remove duplicate state scans, keep checks for distinct high-value states, and consider component tests for reusable UI. Measure suite time before and after changes. |
| Cloud Accessibility report is missing | Cypress Accessibility processes recorded Cypress Cloud run data; local interactive runs do not create those reports. | Check recording, project setup, supported captured browser data, and current Cypress Cloud requirements. See the official FAQ. |
| Cloud report omits an expected rule | The product’s rule defaults may exclude it; the rule set differs from your local plugin configuration. | Check the run’s applied configuration and axe-core version in Cypress Cloud and consult Cypress about supported rule changes. |
Or skip the browser setup
If you need a rendered-page screenshot alongside accessibility work, ScreenshotNeo provides a one-request website screenshot API and MCP server. It does not run Cypress or replace accessibility testing; it can capture a page image for visual review or documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does Cypress accessibility testing require Cypress Cloud?
No. cypress-axe runs scans inside tests. Cypress Accessibility is the paid Cypress Cloud option and processes recorded test data.
Does a passing axe scan mean my site is accessible?
No. It means the configured rules did not report violations in the states scanned. It does not cover every criterion or establish conformance; pair scans with explicit tests and manual evaluation.
Should I scan every page in every test?
Cover the distinct page and interaction states that matter, while avoiding redundant scans of the same state. Include component coverage for shared UI and flow-level coverage where page context adds risk.
Can Cypress test accessibility in component tests?
Yes. With the plugin, inject axe and scan the mounted component. Remember that a component-only scan cannot assess page-wide context that is absent from the mount.
Can screenshots validate accessibility?
A screenshot can support visual review, but it cannot reveal semantic names, keyboard behavior, or how assistive technology exposes the interface. Use it as a companion to tests and human evaluation.


