react nextjs accessibility patterns

Auditing a Third-Party React Component for Accessibility

"Accessible" is the most common word in a component library's README and one of the least reliable. It can mean the component follows the full ARIA pattern with keyboard support and tested announcements, or that someone added role="button" to a <div> two years ago. Since adopting a component is a decision you will be living with for years, twenty minutes of evaluation up front is one of the highest-leverage checks available.

This guide is the script for those twenty minutes. It extends Accessible Component Libraries in React.

WCAG Success Criteria Addressed (what the audit checks):

  • 2.1.1 Keyboard
  • 2.4.3 Focus Order
  • 2.4.7 Focus Visible
  • 4.1.2 Name, Role, Value
A twenty-minute audit, in order of what rules a component out fastest Five checks arranged from cheapest to most expensive. First, five minutes on the keyboard alone, which eliminates most candidates immediately. Second, three minutes with an automated scan on the library's own demo page. Third, five minutes with a screen reader listening for name, role and state. Fourth, five minutes reading the escape hatches: whether ids, labels and refs can be supplied. Fifth, two minutes checking the issue tracker for how accessibility reports are handled. 1 · Keyboard only — 5 minutes Tab, arrows, Enter, Space, Escape. Rules out most candidates before anything else runs. 2 · Automated scan — 3 minutes Run axe on the library's own demo. Violations there are a statement of priorities. 3 · Screen reader — 5 minutes Name, role and state on every control; announcements when things change. 4 · Escape hatches — 5 minutes Can you pass ids, aria-* props, and refs? A closed component cannot be fixed later. 5 · Issue tracker — 2 minutes Search "accessibility": are reports fixed, triaged, or closed as wontfix?

Prerequisites

  • A sandbox where you can render the candidate component in isolation.
  • A screen reader you can drive for a few minutes — see testing with NVDA and VoiceOver in a dev workflow.
  • The list of components you actually need. Auditing a whole library is unnecessary; audit the three components you will use.

Start With the Keyboard, Because It Is Fastest

Five minutes of keyboard testing eliminates more candidates than every other check combined, and it needs no tooling.

Implementation Guidelines:

  • Tab to the component. If you cannot reach it, stop here.
  • Exercise the pattern's expected keys: arrows in a listbox or tab list, Escape in an overlay, Home/End in a long list, Space on any button.
  • Watch the focus ring the entire time. A widget that moves selection without moving a visible indicator will strand sighted keyboard users.
  • Check what happens after an interaction completes: does focus return to the trigger after a dialog closes, or fall to the top of the page?

Scan the Library's Own Demo

The demo page is the library's best foot forward. Violations there are informative in a way violations in your code are not.

Implementation Guidelines:

  • Run axe DevTools on the component's documentation page. Note the rule names as much as the count.
  • Treat button-name, aria-required-children and aria-valid-attr-value failures as serious: they mean the pattern's structure is wrong, not that a demo was sloppy.
  • Ignore contrast findings on a marketing theme; you will supply your own colours.
  • Check the demo's markup in DevTools. A <div role="button" tabindex="0"> where a <button> would do is a signal about how the whole library is built.

Listen for Name, Role and State

Automated tools confirm attributes exist. A screen reader confirms they mean something.

What to listen for on each control Three questions applied to every control in the component. Name asks whether the announcement identifies the control usefully and whether it can be overridden. Role asks whether it matches the behaviour, so a control announced as a button behaves like one. State asks whether expanded, selected or checked is announced and whether it updates when the user changes it. A failing example is shown for each: an unnamed icon button, a div announced as clickable, and a toggle whose state never changes. Name Is it identified usefully? Can you override it? Fails as: "button" with no name Role Does it match behaviour? Is it a native element? Fails as: "clickable" on a div State Is it announced at all? Does it update live? Fails as: "collapsed" forever All three must hold on every control the component renders — including the ones you did not know it rendered.

Implementation Guidelines:

  • Tab through every control the component renders and note the announcement verbatim.
  • Change state and listen again. A state that never updates is worse than none, because it actively misinforms.
  • Trigger the component's dynamic behaviour — open, filter, select — and check whether anything is announced at all.
  • Compare against the ARIA Authoring Practices pattern the component claims to implement. Deviations are not automatically wrong, but they should be deliberate.

Judge the Escape Hatches

You will need to fix something. What matters is whether the API lets you.

Implementation Guidelines:

  • Check whether arbitrary props are forwarded to the underlying element. ...rest spreading is the difference between "we can add an aria-label" and "we cannot".
  • Look for ref forwarding, without which you cannot move focus into or out of the component yourself.
  • Check whether ids are configurable, or generated internally and unreachable — the latter blocks every aria-labelledby relationship you might need.
  • Prefer headless libraries when the audit is close. Owning the markup means owning the fix.
// The three questions in code form.
<Widget
  aria-label="Filter results"        // 1. does it forward aria-* props?
  ref={widgetRef}                    // 2. does it forward refs?
  id="results-filter"                // 3. can I control the id it references?
/>

When the Audit Fails

A failed audit is not automatically a rejection — it is a cost estimate.

Implementation Guidelines:

  • If the component is open and the gap is small, wrap it: add the missing name, manage focus around it, and document the wrapper as the only entry point.
  • If the gap is structural — wrong roles, no keyboard model — do not wrap it. A wrapper cannot add a keyboard contract the component does not have.
  • File the issue upstream with a transcript, whatever you decide. It is how libraries improve, and the response tells you whether to stay.
  • Record the decision where the next person will find it: an ADR, or a comment in the wrapper explaining what was fixed and why.
What to do when the audit fails, by kind of gap Three outcomes. A small gap in an open component, such as a missing accessible name, can be closed with a thin wrapper that supplies the prop and is documented as the only entry point. A structural gap, such as missing keyboard support or wrong roles, cannot be wrapped and means choosing a different component. A gap in a component already in production is triaged by severity: unreachable controls are urgent, missing state announcements are important, imperfect wording can be scheduled. Small gap, open component → wrap it Supply the missing name or manage focus around it; document the wrapper as the only entry point. Structural gap → choose differently A wrapper cannot add a keyboard model or fix roles the component renders itself. Already in production → triage by severity Unreachable control: urgent. Missing state: important. Imperfect wording: scheduled.

How to Verify

  • Re-run the script after adoption. Libraries regress; a component that passed at version 3.1 may not at 4.0.
  • Add a regression test. Assert the accessible name and keyboard behaviour of the wrapped component in your own test suite — see testing React components with jest-axe.
  • Scan in your own theme. Contrast findings that did not apply on the demo may apply with your palette.
  • Check the dark theme. Third-party components frequently assume a light background.
  • Test with your data. Long labels, right-to-left text and empty states expose problems demos never show.
  • Render it inside a form and inside a dialog. Components often behave differently once nested, particularly around focus containment and id generation, and both contexts are common in real applications.
  • Try it with a long label and a short one. Truncation, wrapping and overflow frequently break the accessible name or clip the focus ring in ways a demo with tidy content never shows.

Common Accessibility Mistakes

  • Trusting the README. "Accessible" and "WCAG compliant" are marketing claims with no shared definition.
  • Auditing the whole library. Audit the components you will use; the others are noise.
  • Testing only with a scanner. It cannot hear that a name is meaningless or that a state never updates.
  • Adopting a closed component and planning to fix it later. Without prop forwarding and refs, later never comes.
  • Ignoring the issue tracker. How a project handles accessibility reports predicts your next two years better than its current code.
  • Skipping the re-audit on upgrade. Accessibility regressions are rarely mentioned in changelogs.

Conclusion

Twenty minutes of structured evaluation — keyboard, scan, screen reader, escape hatches, issue tracker — is enough to separate components that implement a pattern from components that mention one. Do it before adoption, when the cost of choosing differently is a paragraph in a decision record rather than a migration. And write down what you found: the next person to evaluate the same library will otherwise start from the README, which is exactly where you started.

Keep the audit results somewhere durable. A short table in the repository — component, version audited, verdict, known gaps, wrapper location — pays for itself the first time someone proposes the same library again, and it turns an upgrade into a diff against a known baseline rather than a fresh investigation. It also gives the accessibility conversation a shared reference point during procurement, which is usually when the decision is actually made and rarely when anyone has twenty minutes to spend on a keyboard.

Frequently Asked Questions

Are headless libraries always more accessible? Not automatically, but they usually fail more honestly: they ship the behaviour and leave the markup to you, so a gap in your markup is your gap to fix. The risk with a fully-styled library is inheriting decisions you cannot reach.

How much should a published VPAT or accessibility statement weigh? It is evidence of intent and effort, not a guarantee. Read what it actually claims — often "supports with exceptions" — and check whether the exceptions cover the component you need. It never replaces the twenty-minute script.

What if the component is already in production and fails? Assess the severity first: an unreachable control is urgent, a missing state announcement is important, a suboptimal announcement can be scheduled. Wrap what you can, file upstream, and put a replacement on the roadmap if the gap is structural.

Should I test every component in a design system I am adopting? Test the ones you will ship, then re-test as you adopt more. A dozen components audited over six months is realistic; a whole library audited in one week produces a document nobody reads.

Is it worth contributing the fix upstream? Often yes — accessibility fixes are usually small, well-scoped and welcomed, and the alternative is maintaining a patch forever. How the project responds to the first pull request also answers the sustainability question better than any README.