intermediate20 min

Accessibility and Testing React Components

Two disciplines every production component needs: making sure everyone can actually use it, and proving it works with more than a manual click-through.

What you'll learn

  • Identify a component missing an accessible name and how to fix it
  • Explain what React Testing Library's query-by-role philosophy encourages
  • Write a deterministic assertion for a piece of component logic without a full test runner

Prerequisites

Explanation

A button that's just a <div onClick={...}> looks identical to a real <button> visually, but is invisible to keyboard users (no Tab focus, no Enter/Space activation) and to screen readers (no announced role). React doesn't automatically make anything accessible — every accessibility guarantee comes from using the right semantic element (<button>, not a styled <div>) and the right ARIA attributes when semantic HTML alone can't express something (aria-label on an icon-only button, aria-live on a region that updates asynchronously). The single most common, cheapest fix: every interactive element needs an accessible name — either its own text content, an aria-label, or an associated <label> for a form field. An icon-only close button with no aria-label announces as just "button" to a screen reader, with no indication of what it does.

React Testing Library's core philosophy is querying the way a real user would perceive the page — by visible text, by ARIA role, by label — rather than by internal implementation details like a CSS class name or a component's internal variable names. getByRole("button", { name: "Sign up" }) finds the element the way a screen reader announces it and the way a sighted user reads it; a test written this way breaks only when the actual user-facing behavior breaks, not when an unrelated internal refactor changes a class name. This is a deliberate design choice, not an arbitrary convention — tests coupled to implementation details are exactly the tests that need constant, unhelpful rewriting every time the code is refactored without any real behavior change.

A full test runner isn't required to practice the underlying discipline of writing a clear, deterministic assertion. This lesson's exercises write plain assertion functions — the same "given this input, assert this specific output" shape that expect(...).toBe(...) uses underneath — to build the habit of testing behavior precisely, which is exactly the skill that transfers directly to real React Testing Library assertions once you're working in a real project.

Example

A minimal, real assertion helper -- the same underlying shape as expect().toBe(), used to check a component's derived output deterministically.

function assertEqual(actual, expected, message) {
  if (actual !== expected) {
    throw new Error(message + " -- expected " + JSON.stringify(expected) + " but got " + JSON.stringify(actual));
  }
  return true;
}

function getButtonAccessibleName(button) {
  return button.ariaLabel || button.textContent || null;
}

const iconOnlyButton = { ariaLabel: null, textContent: "" };
try {
  assertEqual(getButtonAccessibleName(iconOnlyButton), null, "icon-only button should have an accessible name, but doesn't");
} catch (e) {
  console.log(e.message);
}

Try it yourself

Add an ariaLabel to iconOnlyButton (e.g. 'Close') and re-run -- the assertion should now need updating to expect that label.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Using getButtonAccessibleName already defined, check three buttons and store whether each has a real accessible name: hasNameA (textContent: 'Sign up', no ariaLabel), hasNameB (icon-only, ariaLabel: 'Close menu'), hasNameC (icon-only, no ariaLabel, no textContent).

Checks: button with text content has a name · icon-only button with aria-label has a name · icon-only button with neither is correctly flagged

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write assertContainsText(renderedOutput, expectedText) that throws an Error with a clear message if expectedText is not found within renderedOutput (a string), and returns true if it is found -- modeling the shape of a React Testing Library text assertion.

Checks: returns true when the text is present · throws a clear error when the text is missing

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Using a styled `<div onClick={...}>` instead of a real `<button>`, losing keyboard focus and activation for free.
  • Leaving an icon-only button with no `aria-label`, so it announces as an unlabeled, meaningless control to screen reader users.
  • Writing tests that query by CSS class name or internal component structure instead of by role/label/text, making them break on unrelated refactors.

Knowledge check

Knowledge check

1. Why does a `<div onClick={...}>` styled to look like a button fail accessibility even though it looks identical?
2. Why does React Testing Library encourage querying by role, label, or visible text instead of CSS class or internal structure?
3. What is the single cheapest, most common accessibility fix mentioned in this lesson?

Takeaway

Accessibility comes from real semantic elements and explicit accessible names, not automatically from React — and testing by role/label/text (the way a real user perceives the page) keeps tests coupled to behavior, not implementation details.

Summary

This lesson covered identifying missing accessible names, the philosophy behind React Testing Library's role-based queries, and practiced writing clear, deterministic assertions.

References

Your notes

Notes save automatically.