JSX Accessibility
Building React applications with JSX gives us a powerful, declarative way to express user interfaces. But if we stop at “it looks right in the browser,” we exclude users who rely on assistive technologies—and miss out on better SEO and machine‐readable markup. In this article I explained how to write an accessible JSX, with practical examples, so that:
- Developers can ship inclusive UI that meets Web Content Accessibility Guidelines (WCAG) guidelines.
- Crawlers (search engines, chatbots, indexing tools) can parse your web components reliably and extract content in the right semantic structure.
Why Infuse Accessibility into JSX
-
According to the United Nations's report, over 1 billion people worldwide have disabilities; many rely on screen readers, keyboard navigation, or simplified layouts.
-
Search engines and crawlers including your AI chatbots (ChatGPT, Gemini, etc.), use your markup to understand page structure. Proper tags (
<header>
,<nav>
, headings) and attributes (alt
,aria-label
) boost discoverability and rich results. -
Accessible components are usually more semantic, better documented, and easier to test with tools like axe-core or eslint-plugin-jsx-a11y.
Principles to follow to Have an Accessible JSX
Follow the POUR principles from WCAG:
-
Perceivable: Always provide text alternatives (
alt
,aria-label
) and ensure efficient color contrast (use tools to verify 4.5:1 ratio). -
Operable: All interactive elements must be keyboard-focusable (use
<button>
not<div>
) with visible focus indicators. -
Understandable: Use clear labels and consistent component behavior. Also, let your error messages be tied to inputs via
aria-describedby
. -
Robust: Always use semantic HTML inside your JSX and only fall back to ARIA roles when necessary.
Practical Examples
1. Semantic Buttons & Links
Bad (non-semantic, not keyboard‐friendly):
<div onClick={handleSubmit} role="button">
Submit
</div>
Good (semantic):
<button onClick={handleSubmit}>
Submit
</button>
Why?:
<button>
natively supports keyboard events (Enter
/Space
) and exposes the right role to screen readers.
2. Images with Alternatives
Bad (missing alt):
<img src="/logo.png" />
Good (descriptive alt):
<img src="/logo.png" alt="ayodele's travel marketplace logo" />
Tip: If the image is purely decorative, use
alt=""
so screen readers could skip it.
3. Form Inputs & Labels
Bad (unlinked label):
<input id="email" type="email" />
<label>Email</label>
Good (explicit association):
<label htmlFor="email">Email address</label>
<input id="email" type="email" />
Or using wrapper:
<label>
Email address
<input type="email" name="email" />
</label>
Why?: Explicit labels improve click targets and announce the input purpose.
4. ARIA for Custom Components
When creating a custom dropdown, native <select>
isn’t enough. You need ARIA:
function AccessibleDropdown({ options, value, onChange }) {
return (
<div role="combobox"
aria-haspopup="listbox"
aria-expanded={Boolean(value)}
aria-controls="my-listbox"
tabIndex={0}
onKeyDown={handleKey}>
{value || 'Select an option'}
<ul role="listbox" id="my-listbox">
{options.map(opt => (
<li key={opt} role="option"
aria-selected={opt === value}
onClick={() => onChange(opt)}
>
{opt}
</li>
))}
</ul>
</div>
)
}
Note: Ensure
tabIndex
,role
,aria-*
, and keyboard handlers align exactly with WAI-ARIA Authoring Practices.
Keyboard Navigation
Always ensure that your tab order is logical (avoid positive tabIndex
) and let your focus ring be visible. You can as well customize via CSS if needed:
```css
:focus {
outline: 3px solid Highlight;
outline-offset: 2px;
}
```
Color Contrast & Visual Cues
Don’t rely solely on color:
<button disabled style={{ color: 'gray' }}>
Save
</button>
Add text or icon:
<button disabled aria-disabled="true">
<LockIcon aria-hidden="true" /> Save
</button>
Testing & Tooling
-
You can add
eslint-plugin-jsx-a11y
to catch missingalt
,aria-*
, for inappropriate use of interactive roles. -
You can integrate
axe-core
for automated audits into your test suites:import { axe, toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations); test('Homepage is accessible', async () => { const { container } = render(<HomePage />); const results = await axe(container); expect(results).toHaveNoViolations(); });
-
Perform manual keyboard navigation, VoiceOver/NVDA walkthrough.
Helping AI Crawlers
Well-structured JSX also aids machine learning agents:
- Headings (
<h1>…<h6>
) reveal document hierarchy. - Landmarks (
<header>
,<nav>
,<main>
) let crawlers segment content. - Metadata: use
<meta name="description">
and<link rel="canonical">
in your React Helmet or Next.js<Head>
. - Structured data: JSON-LD in
<script type="application/ld+json">
for rich snippets.
Example in Next.js:
import Head from 'next/head';
export default function BlogPost({ post }) {
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"datePublished": post.date,
// …other fields
}) }}
/>
</Head>
<article>
<h1>{post.title}</h1>
{/* ... */}
</article>
</>
);
}
Conclusion
Accessible JSX is more than compliance; it makes your codebase future-proof, enhances SEO, and makes it easier for everyone to use. Using semantic HTML, best practices for ARIA, and testing, you can make React components that both humans and machine learning agents can interact with.