Component libraries fail accessibility in a very specific way: they usually start out close to correct, then drift. A button loses its visible focus ring after a refactor, a dialog traps focus in Chrome but not Safari, a menu still works with a mouse but no longer responds to Shift+Tab in a nested state, or a reusable form field quietly drops its label when a prop changes shape. Those changes are easy to miss in a component showcase, and they often slip past manual review because the UI still looks fine at a glance.

That is why teams need a deliberate way to test accessibility regressions in component libraries before the broken behavior spreads into every product surface that consumes those components.

The goal is not to replace accessibility audits or full end-to-end testing. The goal is to catch drift at the component layer, where the blast radius is smallest and the feedback cycle is fastest. If you can validate the keyboard flow, focus order, ARIA wiring, and visible state transitions of each reusable component in real browsers, you can prevent a large share of downstream accessibility bugs.

Why component libraries regress differently from product apps

A product page usually exercises a few component instances in one context. A component library, design system, or UI kit is reused everywhere, which creates a different kind of risk.

Common regression patterns

  • Props change behavior subtly, for example a variant prop changes DOM structure and accidentally removes the label association.
  • Slots or children patterns create incomplete markup, especially when consumers supply custom content.
  • State classes drift from semantics, where visual states update but ARIA state does not.
  • Keyboard support breaks in one branch, such as menus, tabs, comboboxes, dialogs, and date pickers.
  • Styling changes hide focus indicators, especially after token updates or CSS resets.
  • Cross-browser behavior diverges, especially in Safari and Firefox, where focus and key event behavior can differ from Chromium.

If a reusable component is wrong once, every consuming app inherits the bug.

That is why the right test strategy is layered. You want static checks, interaction tests, accessibility engine scans, and a browser-based regression layer that exercises the component the way users do, not just the way developers expect it to work.

Start with the accessibility contract, not the implementation

Before writing tests, define what each component promises from an accessibility standpoint. This does not need to be a formal spec document, but it should exist somewhere durable, ideally alongside component docs.

For each component, capture:

  • Role and semantics, such as button, dialog, tablist, menu, textbox, combobox.
  • Required labels, visible text, aria-label, aria-labelledby, or associated <label>.
  • Keyboard interactions, for example Enter and Space activate a button, arrow keys move between tabs, Escape closes a dialog.
  • Focus behavior, including where focus lands on open, close, next, and previous navigation.
  • Error and help text behavior, including how validation messages are exposed.
  • Visible states, such as hover, active, disabled, selected, expanded, pressed, invalid, and busy.
  • Browser-specific edge cases, especially if a pattern is known to behave differently across engines.

If the team agrees on the contract first, the tests become much easier to maintain. Otherwise, you end up encoding unclear behavior into brittle assertions.

Use a layered test strategy

The most reliable way to catch regressions is to spread coverage across a few test types, each with a distinct purpose.

1. Static checks for markup and authoring rules

Static linting and type checking are good at catching missing labels, invalid aria-* attributes, incorrect element nesting, and bad component API changes. Tools like eslint plugins, TypeScript, and automated accessibility linters can fail fast before a browser ever opens.

Static checks are especially useful for rules that do not require interaction, such as:

  • A custom input must expose a programmatic label.
  • A dialog must include a heading or accessible name.
  • A button must not contain invalid interactive descendants.
  • A component must preserve aria-disabled and native disabled semantics consistently.

These checks are necessary, but not sufficient. They do not prove that keyboard navigation works or that focus order remains intact after the component opens, closes, expands, or re-renders.

2. Component-level interaction tests

Run focused browser tests against each component in isolation. This is where you validate keyboard navigation testing, focus restoration, and state changes.

Typical checks include:

  • Tab enters the component at the expected point.
  • Arrow keys move focus correctly within composite widgets.
  • Escape closes overlays and returns focus to the trigger.
  • Disabled controls do not accept activation.
  • Visible focus stays on the active element.

Playwright is a strong fit here because it can inspect the accessibility tree, press keys, and run in multiple browsers.

import { test, expect } from '@playwright/test';
test('dialog traps focus and restores it on close', async ({ page }) => {
  await page.goto('/storybook/?path=/story/dialog--default');
  await page.getByRole('button', { name: 'Open dialog' }).click();

const dialog = page.getByRole(‘dialog’, { name: ‘Settings’ }); await expect(dialog).toBeVisible(); await expect(page.getByRole(‘button’, { name: ‘Save changes’ })).toBeFocused();

await page.keyboard.press(‘Escape’); await expect(dialog).toBeHidden(); await expect(page.getByRole(‘button’, { name: ‘Open dialog’ })).toBeFocused(); });

This kind of test catches behavior that static analysis cannot see.

3. Full browser regression checks

This is where a browser-based regression layer adds real value. Instead of only checking a single scripted path, validate the component in actual browsers with the viewport, rendering, and focus behavior users will experience.

For teams that want browser-based regression coverage without maintaining a heavy local infrastructure, Endtest, an agentic AI [Test automation](https://en.wikipedia.org/wiki/Test_automation) platform, is a relevant option. It runs tests across real browsers and can be used as a validation layer for keyboard flows, focus order, and visible UI states. Endtest also supports accessibility checks through its web tests, using Axe-based rules for WCAG violations, ARIA issues, and common markup problems, which can complement your scripted component tests. If you use it, keep the tests focused on the behavior contract, not on implementation details.

4. Audits for deeper manual review

No automation stack replaces human review completely. Use manual audits for complex patterns, advanced screen reader behavior, and UX decisions that need judgment. The automated suite should reduce the volume of manual work, not pretend to eliminate it.

What to test for each kind of component

Different component families fail in different ways. A useful test matrix maps each pattern to its most important accessibility behaviors.

Buttons and toggles

For buttons, test:

  • Correct role and accessible name.
  • Keyboard activation with Enter and Space.
  • Visible focus.
  • Disabled state behavior.
  • Toggle buttons exposing aria-pressed when appropriate.

A common regression is a visual button that becomes a div during refactoring, then relies on click handlers alone. That may still work with a mouse, but it usually fails keyboard support and semantics.

Dialogs and drawers

For dialogs, test:

  • The trigger opens the dialog.
  • Focus moves into the dialog immediately.
  • Focus does not escape while open.
  • Escape closes the dialog.
  • Focus returns to the trigger or a clearly defined element.
  • The accessible name is stable.

Also test scroll locking and layering. A dialog that appears visually correct may still leave hidden content tabbable in the background.

Tabs, menus, and composite widgets

Composite widgets often have the highest regression risk because they depend on keyboard patterns rather than simple click behavior.

For tabs, test:

  • Only the active tab is tabbable, unless your pattern intentionally differs.
  • Arrow keys move between tabs.
  • The selected tab and tabpanel are associated properly.
  • Focus and selection do not drift apart unexpectedly.

For menus and menu buttons, test:

  • Trigger button announces expanded state correctly.
  • Arrow keys move through menu items.
  • Escape closes the menu.
  • Clicking outside or tabbing away behaves as documented.

Forms and validation

For forms, test:

  • Labels are associated correctly.
  • Required fields announce required status.
  • Validation errors are linked to fields.
  • Error summaries, if used, are reachable and useful.
  • Placeholder text is not used as the only label.

This is one of the easiest places for accessibility regressions to sneak in, because teams often optimize form layout and styling without rechecking the accessible name relationship.

Tables, lists, and data display

For data-heavy components, test:

  • Header associations are preserved.
  • Sorting controls expose state.
  • Row and column structure remains semantic.
  • Actions in cells remain keyboard reachable.

If the component library uses div-based grids for layout flexibility, make sure the semantics still communicate the structure a user needs.

Build tests around user journeys, not DOM snapshots

Snapshot testing has a place, but it is weak at catching accessibility regressions by itself. A snapshot can tell you the DOM changed. It cannot tell you whether focus now lands in the wrong place, or whether a visual style removed the only usable focus indicator.

Prefer assertions that reflect user-facing behavior:

  • getByRole() and getByLabel() queries.
  • Focus assertions.
  • Keyboard interactions.
  • State changes exposed through ARIA.
  • Contrast and violation checks from an accessibility engine.

The best accessibility regression test is usually a short interaction sequence with a few strong assertions, not a long script full of incidental detail.

Make component testing browser-aware

A component library often passes in Chromium and still fails in Safari or Firefox. That is one reason browser coverage matters, especially for focus behavior, native controls, and text input quirks.

Use cross-browser runs for components with known browser-sensitive behavior, such as:

  • :focus-visible styling.
  • Date inputs and time pickers.
  • Custom selects and comboboxes.
  • Sticky overlays and scroll behavior.
  • Keyboard events combined with IME or text composition.

If you are comparing browsers, cross-browser testing guidance from Endtest can be useful as a browser-based regression layer, particularly when you want tests to run on real browsers rather than approximations. That matters for accessibility work, because focus order and keyboard handling can differ in subtle ways.

Where accessibility engine scans fit

An accessibility engine scan is good at catching known classes of violations, especially if it is run regularly on the component library itself. Axe-based checks can detect things like missing labels, invalid ARIA, heading structure problems, empty buttons, and contrast issues.

These checks are valuable, but they should be framed correctly:

  • They catch violations, not every usability issue.
  • They do not guarantee keyboard flows are correct.
  • They do not validate that your UX contract is coherent.

That is why a11y regression checks should run alongside interaction tests. A scan can tell you a dialog is missing an accessible name. A keyboard test can tell you it no longer returns focus to the button that opened it.

If you want a browser-based accessibility step inside broader web tests, Endtest supports accessibility checks in its web test flow, with the option to scan full pages or specific elements and capture violations in the report. The documentation page is the place to start if you want to see how that fits into an existing regression pipeline.

A practical workflow for design system teams

A maintainable workflow usually looks like this:

Step 1, define the component contract

For each component, write down the expected keyboard, focus, and semantic behavior. Keep it concise and close to the code.

Step 2, add unit-level authoring checks

Use lint rules, TypeScript types, or small unit tests to prevent obviously invalid patterns.

Step 3, add interaction tests in a browser runner

Use Playwright, Cypress, or Selenium-based flows for critical interactions. Focus on user behavior, not internal component state.

Step 4, run accessibility checks in CI

Execute accessibility scans on the library pages, storybook stories, or component harnesses. Fail builds on new high-severity issues, or introduce a soft gate first if you are cleaning up a large legacy surface.

Step 5, run a real-browser regression layer

Validate a smaller set of high-value flows across browsers and viewports. This is especially important for overlays, keyboard navigation, focus restoration, and visible state transitions.

Step 6, review diffs in PRs

When a PR changes a shared component, require evidence that the accessibility contract still holds. That can be a test report, a focused screenshot diff, or a short checklist of behavior validated in review.

How to choose what belongs in CI

Not every accessibility test belongs in the same pipeline stage.

Fast checks for every pull request

Use these for immediate feedback:

  • Linting and type checking.
  • Unit and component tests for core semantics.
  • Targeted a11y regression checks on modified components.

Broader browser checks on merge or nightly

Use these when you need more confidence but can tolerate longer execution time:

  • Cross-browser component flows.
  • Full library accessibility scans.
  • Visual state verification across key breakpoints.

Manual review for complex changes

Reserve manual audits for cases such as:

  • A new interaction pattern.
  • A major refactor of focus management.
  • A change that affects screen-reader announcement order.
  • A new composite widget with a nontrivial keyboard model.

A sample CI pattern

A simple GitHub Actions workflow can split fast checks from browser-based checks:

name: component-accessibility

on: pull_request: push: branches: [main]

jobs: fast-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run lint - run: npm test

browser-checks: runs-on: ubuntu-latest if: github.event_name == ‘push’ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run test:playwright

This split keeps pull request feedback fast while reserving heavier browser coverage for the branch where you are more likely to accept the runtime cost.

Common failure modes and how to avoid them

Over-testing implementation details

If your test clicks internal wrappers, checks class names, or depends on DOM structure that consumers never see, refactors will break tests without improving confidence. Query by role, label, and visible text whenever possible.

Treating accessibility as a one-time audit

Accessibility drift is a maintenance problem, not a launch problem. Even a strong audit at release time will miss later regressions if you do not keep checking the library as it evolves.

Testing only happy paths

A dialog that opens correctly may still fail when it is closed with Escape. A menu that works with arrow keys on the first open may fail after rerendering. A text field may announce the label but lose the error state after async validation. Add edge-case paths deliberately.

Ignoring browser differences

Keyboard and focus regressions are often browser-specific. If your design system supports multiple browsers, test the components in the browsers your users actually have.

Letting accessibility checks become noisy

If every PR produces a long list of pre-existing violations, the team will stop trusting the suite. Start with the components most likely to regress, classify known issues, and ratchet the threshold over time.

How AI-generated tests fit, carefully

Teams are increasingly using AI-generated tests to accelerate coverage creation, especially for repetitive component patterns. That can be useful, but only if the output is reviewed against the component contract.

The risk is that generated tests often mirror the surface structure of the demo, not the accessibility behavior you actually care about. A good AI-assisted workflow can draft the first pass of a dialog, tab, or form test, then a human refines the assertions to cover focus restoration, keyboard support, and state exposure.

This is where a platform like Endtest can fit as a browser-based regression layer, because it combines low-code workflows with editable steps and browser execution. Used well, it can reduce setup overhead without removing the need for an engineer or tester to define the accessibility behavior clearly.

A checklist you can reuse

If you need a short operational checklist for each shared component, start here:

  • Does the component expose the correct role and accessible name?
  • Can it be reached and operated entirely with a keyboard?
  • Does focus move to the expected place when it opens or changes state?
  • Does focus return to a sensible place when it closes or resets?
  • Are visible states aligned with ARIA states?
  • Do labels, descriptions, and errors remain attached after rerenders?
  • Does the behavior work in the browsers you support?
  • Are a11y regression checks part of the CI path, not a manual afterthought?

Final thought

The best way to prevent accessibility regressions in a component library is to treat accessibility as a behavioral contract, then test that contract continuously at the component layer and again in real browsers. Static checks catch bad markup, interaction tests catch keyboard and focus failures, and browser-based regression runs catch the gaps between the two.

If your team wants to test accessibility regressions in component libraries without building a large custom harness for every browser combination, keep the core strategy the same, then choose tools that support it well. Whether you use Playwright, Cypress, Selenium, or a browser-based platform like Endtest, the important part is not the tool name. It is whether your tests prove that users can still navigate, perceive, and operate the shared UI after every change.