Ayodele Aransiola

JSX Accessibility

July 23, 2025 By Ayodele
jsx
accessibility
react
a11y

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

  1. According to the United Nations's report, over 1 billion people worldwide have disabilities; many rely on screen readers, keyboard navigation, or simplified layouts.

  2. 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.

  3. 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:

  1. Perceivable: Always provide text alternatives (alt, aria-label) and ensure efficient color contrast (use tools to verify 4.5:1 ratio).

  2. Operable: All interactive elements must be keyboard-focusable (use <button> not <div>) with visible focus indicators.

  3. Understandable: Use clear labels and consistent component behavior. Also, let your error messages be tied to inputs via aria-describedby.

  4. 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 missing alt, 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.