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.
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.
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.
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
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.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.