ScreenshotNeo

BlogHow-to

How to Find Hidden Accessibility Issues with Cypress

Catch accessibility failures that appear after user actions with Cypress, axe-based scans, targeted assertions, and manual checks.

By the ScreenshotNeo team4 October 20267 min read

To find accessibility issues that only appear after interaction, make Cypress visit each important UI state, then run an accessibility scan in that state and assert the behavior your product expects. An initial-page scan cannot inspect a menu, dialog, validation error, or dynamic result that has not appeared yet. Automated scans are useful, but they do not prove that an application is accessible; keyboard and assistive technology review still matter.

1. Identify the states users can reach

List journeys that change what is rendered or announced. Include menus, dialogs, disclosure panels, form errors, autocomplete results, tabs, filters, and loading or success messages where relevant. For each journey, note the expected accessible name, state, focus destination, keyboard behavior, and status message.

“Hidden” has two meanings here. A control can be absent from the initial state and appear after an action; a test that never performs that action has a coverage gap. Separately, Cypress visibility assertions determine whether an element meets Cypress’s visibility rules. Passing a visibility assertion says nothing by itself about its accessible name, keyboard path, or announcements.

2. Add cypress-axe scans at meaningful checkpoints

The community cypress-axe integration injects axe-core and provides commands for scanning the current page or component state. Add the integration using the instructions for your installed versions, then visit a state before calling the scan. The example below assumes the plugin’s commands have been installed according to its documentation.

// cypress/support/e2e.js
import 'cypress-axe';

// cypress/e2e/navigation.cy.js
describe('navigation accessibility', () => {
  it('checks the opened menu', () => {
    cy.visit('/');
    cy.get('[data-cy="menu-button"]').click();
    cy.get('[data-cy="main-menu"]').should('be.visible');
    cy.injectAxe();
    cy.checkA11y('[data-cy="main-menu"]');
  });
});

Use a scope selector when the question is about one component; omit it when you intend to scan the whole rendered page. A scoped scan can reduce unrelated findings, but it can also miss problems outside that region. Keep the scope deliberate and cover other relevant regions in their own checkpoints.

For a dialog, make the open and close transitions part of the journey, and check the expected focus behavior with assertions appropriate to your app:

cy.get('[data-cy="open-dialog"]').click();
cy.get('[role="dialog"]').should('be.visible');
cy.focused().should('have.attr', 'data-cy', 'dialog-close');
cy.injectAxe();
cy.checkA11y('[role="dialog"]');

cy.get('[data-cy="dialog-close"]').click();
cy.get('[role="dialog"]').should('not.exist');

Adapt selectors and focus expectations to the application. A dialog may keep its node mounted while closed, so not.exist may be the wrong assertion; test the actual hidden or closed state your implementation uses.

3. Assert the intended accessible behavior

A generic rule engine cannot infer the name or behavior your product intends for a particular control. Add ordinary Cypress assertions for requirements specific to the journey, such as the expected accessible name, expanded or selected state, focus destination, keyboard operation, and status messaging.

cy.get('[data-cy="menu-button"]')
  .should('have.attr', 'aria-label', 'Open main menu')
  .and('have.attr', 'aria-expanded', 'true');

cy.get('[data-cy="search"]')
  .type('keyboard');
cy.get('[data-cy="search-results"]')
  .should('be.visible');
cy.get('[data-cy="search-status"]')
  .should('contain.text', 'results');

These are examples, not universal expected values. Match assertions to the labels, states, and interaction contract your interface is designed to provide. Cypress also documents using an ordinary assertion to verify that a button has the accessible name users should hear.

4. Understand visibility assertions

As of Cypress 16, Cypress’s default visibility algorithm delegates to the browser’s native Element.checkVisibility(). Cypress documents hidden conditions that include zero dimensions, display: none on the element or an ancestor, hidden or collapsed visibility, and certain content-visibility cases. Opacity is treated as hidden when directly asserting visibility.

Use should('be.visible') to confirm that the test reached the intended rendered state. Do not use it as an accessibility verdict. An element can be visible and still have no useful name, an incorrect role or state, or an unusable keyboard interaction. Behavior can also vary with Cypress version, so consult the documentation matching the version in your project.

5. Choose where automated checks run

Approach Where checks run Test changes Useful distinction
cypress-axe Inside a Cypress test, at the point where you call the scan Install the community integration and add explicit injection and scan commands Lets the test author choose state, timing, and scope; scan calls add runtime overhead as rules evaluate applicable DOM elements
Cypress Accessibility In Cypress Cloud, using snapshots from recorded runs Cypress documents analysis without adding cy. commands Provides Cloud reporting; it is a paid Cypress product

Choose based on whether you need checks and gating in test code, Cloud reporting, your recording workflow, and your budget. Either way, make sure the captured or scanned states include the interactions that matter.

6. Review the configured rules and scope

Do not describe a passing scan as proof that a suite “passes WCAG.” Cypress Accessibility’s documented default axe-core rules cover WCAG 2.0 and 2.1 Level A and AA and Deque Best Practices. Three WCAG-tagged rules—color-contrast, no-autoplay-audio, and meta-refresh—are off by default. WCAG 2.2, AAA, experimental, and deprecated groups are also off by default unless enabled for the project. Cypress documents that page-level rules do not run for component tests.

Those settings apply to the Cypress Accessibility product as documented; check your project’s actual configuration and installed versions before drawing conclusions. Report which states, rules, and test types your checks cover.

7. Manually evaluate the journeys

  • Use the interface with a keyboard. Check that controls can be reached and operated, focus order makes sense, and focus remains visible.
  • Inspect dialog and menu focus behavior, including where focus goes on open and where it returns on close.
  • Use suitable assistive technology to check names, roles, states, and announcements for dynamic content.
  • Check that instructions, labels, and status messages make sense in context, not just that a rule reports no violation.
  • Involve disabled users where practical, and state the scope of any evaluation.

W3C cautions that tools cannot check every accessibility aspect automatically and that human judgment is required. Automated findings help identify barriers; people still need to evaluate the experience.

8. Troubleshoot common problems

Symptom Likely cause What to do
The scan reports no issues, but users still encounter a barrier The test scanned only the initial or wrong state, or the issue is outside automated rule coverage Perform the user action first, verify the state is present, scan at that checkpoint, and add behavior assertions and manual review
A menu or dialog is missing from scan results The UI was never opened, the scan ran too early, or the scan selector excluded it Trigger the interaction, assert the component is ready, then scan it or the page
A visibility assertion fails The element is not rendered as expected, has zero dimensions, is hidden by an ancestor, or has a visibility state Cypress treats as hidden Inspect the rendered state and CSS, wait for the intended transition, and check the Cypress version’s visibility behavior
A scan command is unavailable The community integration was not imported or configured for the relevant Cypress support file Follow the integration instructions for the project’s installed Cypress setup and confirm the support file is loaded
Findings differ between page and component tests Test scopes and applicable rules differ; Cypress Accessibility does not run page-level rules for component tests Review which test type produced the snapshot and which rules are enabled for that scope
Scans make the suite slower Rules evaluate applicable DOM elements at each scan call Scan meaningful checkpoints rather than after every assertion, and keep component scope intentional
A passing result is being treated as full WCAG conformance The configured rules omit some rule groups, and automated checks cannot evaluate every aspect Document the rules and states covered, enable needed groups where appropriate, and complete human evaluation

9. Keep the suite useful and reliable

Prefer stable selectors and explicit state assertions over arbitrary delays. Run scans after the relevant UI has settled, and avoid duplicate scans of the same unchanged state unless they answer a distinct question. Dynamic interfaces may need a clear readiness signal before scanning. Scan scope and scan frequency both affect feedback time; each in-test call has runtime overhead.

Accessibility coverage is only as reliable as the journeys and states represented in the suite. Keep those journeys aligned with changes to menus, dialogs, forms, and dynamic content. Treat scanner output as findings to investigate, and distinguish a clean configured scan from a complete accessibility evaluation.

Or skip the browser setup

If your Cypress work also needs screenshots of the rendered pages or states, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. The screenshot call is not an accessibility scan and does not replace the Cypress workflow above.

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 request options. Cookie banners, popups, and chat widgets are removed before the shot, and each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing result. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free account and get 1,000 screenshots a month with no card.

FAQ

Does an axe scan inspect every hidden element?

A scan examines the current rendered page or component state. Make the relevant interface state appear before scanning; a one-time scan cannot cover states the test never reaches.

Can an automated Cypress scan prove my app is accessible?

No. It can find some rule violations in the states and rules it checks. Keyboard and assistive technology evaluation, human judgment, and practical user feedback cover aspects automated tools cannot determine.

Should I use cypress-axe or Cypress Accessibility?

Decide based on whether you want explicit in-test scans or Cloud analysis of recorded snapshots, along with your reporting, pipeline, and budget needs.

Does be.visible mean a control is accessible?

No. It checks Cypress’s visibility criteria, not the control’s name, keyboard operation, focus behavior, or announcements.