Reviews accessibility including WCAG, ARIA, keyboard navigation. Use when junior builds forms, buttons, modals, interactive elements, or asks "is this accessible", "a11y", "screen reader".
"Accessibility is not a feature, it's a requirement. If 15% of users can't use your app, you've failed 15% of users."
Activate this skill when:
<button> not <div onClick><label>aria-labelaria-describedbyoutline: none without replacement<button> or <a>// ❌ BAD: Not keyboard accessible, no semantics
<div onClick={handleClick} className="button">
Click me
</div>
// ✅ GOOD: Native button element
<button onClick={handleClick} className="button">
Click me
</button>
Why it matters: <div onClick> doesn't receive keyboard focus, doesn't respond to Enter/Space, and isn't announced as a button by screen readers.
// ❌ BAD: Input has no label
<input type="email" placeholder="Email" />
// ✅ GOOD: Label linked to input
<label htmlFor="email">Email</label>
<input id="email" type="email" placeholder="example@email.com" />
// ✅ ALSO GOOD: Wrapping label
<label>
Email
<input type="email" />
</label>
// ❌ BAD: No accessible name
<button onClick={handleDelete}>
<TrashIcon />
</button>
// ✅ GOOD: ARIA label for screen readers
<button onClick={handleDelete} aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
/* ❌ BAD: Focus invisible */
button:focus {
outline: none;
}
/* ✅ GOOD: Custom but visible focus */
button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
}
/* ✅ BEST: Use focus-visible */
button:focus-visible {
outline: 2px solid #4299e1;
outline-offset: 2px;
}
// ❌ BAD: "Click here" tells screen reader nothing
<p>
To read our privacy policy, <a href="/privacy">click here</a>.
</p>
// ✅ GOOD: Link text describes destination
<p>
Read our <a href="/privacy">privacy policy</a>.
</p>
// ❌ BAD: Screen reader can't navigate
<div className="title">Welcome</div>
<div className="subtitle">Getting Started</div>
// ✅ GOOD: Proper headings
<h1>Welcome</h1>
<h2>Getting Started</h2>
Ask these instead of giving answers:
# In your test file
# Pattern: axe-core for React Testing Library
import { axe } from 'jest-axe';
it('should have no a11y violations', async () => {
const { container } = render(<YourComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
| Attribute | Use Case |
|---|---|
aria-label |
Provides name for icon-only buttons |
aria-labelledby |
Points to element with visible label |
aria-describedby |
Points to description (error messages) |
aria-hidden="true" |
Hides decorative icons from screen readers |
aria-expanded |
Indicates dropdown/accordion state |
aria-live |
Announces dynamic content changes |
role |
Defines element's purpose (use sparingly) |
"No ARIA is better than bad ARIA."
Use semantic HTML first. Only use ARIA when HTML can't express what you need.
// Pattern: Button with accessible name
<button
onClick={handleAction}
aria-label="Close modal"
>
<XIcon aria-hidden="true" />
</button>
// Pattern: Error linked to input
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-describedby={error ? "email-error" : undefined}
aria-invalid={error ? "true" : undefined}
/>
{error && (
<span id="email-error" role="alert">
{error}
</span>
)}
| Flag | Question |
|---|---|
<div onClick> |
"What happens when a keyboard user tries to click this?" |
outline: none |
"How does a keyboard user know where they are?" |
| No form labels | "How does a screen reader know what this input is for?" |
| Icon-only button | "What does a screen reader announce for this button?" |
| Color as only indicator | "What if someone is red-green colorblind?" |
tabIndex > 0 |
"This breaks natural tab order. Why is it needed?" |
"I implemented accessibility best practices including semantic HTML, proper form labeling, and keyboard navigation, ensuring our app is usable by everyone."
STAR story material:
Fetch: WAI-ARIA practices
Fetch: React accessibility documentation
Search: "aria-label" + "button" patterns
Search: Modal focus trapping implementations