We use cookies to enhance your experience on the site
CodeWorlds

Accessibility (a11y) in React

On a space station, every crew member must have access to the control systems - regardless of whether they operate them by sight, touch, or voice commands. Accessibility (a11y for short) in web applications works exactly the same way. When building a React interface, we must ensure that every user can use it - blind people using screen readers, people who navigate only by keyboard, or people with color vision deficiencies.

Why is accessibility important?

According to WHO, over a billion people worldwide live with some form of disability. This means that an inaccessible application excludes a huge group of potential users. Beyond the ethical aspect, accessibility:

  • Is legally required in many countries (WCAG, ADA)
  • Improves SEO and overall code quality
  • Makes the application easier to use for ALL users (e.g., on mobile devices)
  • Makes the interface more predictable and testable

Semantic HTML in React

The foundation of accessibility is using the right HTML elements. React encourages creating components, but it is easy to fall into the trap of overusing

<div>
:

1// BAD - "div soup" - screen reader doesn't understand the structure
2function BadNavigation() {
3  return (
4    <div className="nav">
5      <div className="nav-item" onClick={goHome}>Home</div>
6      <div className="nav-item" onClick={goMissions}>Missions</div>
7      <div className="nav-item" onClick={goCrewPanel}>Crew</div>
8    </div>
9  );
10}
11
12// GOOD - semantic HTML elements
13function GoodNavigation() {
14  return (
15    <nav aria-label="Station main menu">
16      <ul>
17        <li><a href="/">Home</a></li>
18        <li><a href="/missions">Missions</a></li>
19        <li><a href="/crew">Crew</a></li>
20      </ul>
21    </nav>
22  );
23}

Key semantic elements include:

<nav>
,
<main>
,
<header>
,
<footer>
,
<section>
,
<article>
,
<aside>
,
<button>
,
<form>
,
<label>
.

ARIA attributes in React

ARIA (Accessible Rich Internet Applications) is a set of attributes that add information about the role and state of elements for assistive technologies. In React, we use them in camelCase or with hyphens:

1function MissionAlert({ message, severity }) {
2  return (
3    <div
4      role="alert"
5      aria-live="assertive"
6      aria-atomic="true"
7      className={severity}
8    >
9      {message}
10    </div>
11  );
12}
13
14function SearchField() {
15  const [results, setResults] = useState([]);
16  const [isOpen, setIsOpen] = useState(false);
17
18  return (
19    <div>
20      <label htmlFor="galaxy-search">Search galaxies:</label>
21      <input
22        id="galaxy-search"
23        type="search"
24        role="combobox"
25        aria-expanded={isOpen}
26        aria-controls="search-results"
27        aria-autocomplete="list"
28        aria-haspopup="listbox"
29      />
30      {isOpen && (
31        <ul id="search-results" role="listbox">
32          {results.map(r => (
33            <li key={r.id} role="option">{r.name}</li>
34          ))}
35        </ul>
36      )}
37    </div>
38  );
39}

Most commonly used ARIA attributes in React:

| Attribute | Use case | |-----------|----------| |

role
| Defines the element's role (alert, dialog, tabpanel) | |
aria-label
| Text label for the element | |
aria-labelledby
| ID of the element that serves as the label | |
aria-describedby
| ID of the element with additional description | |
aria-live
| Announces dynamic content changes | |
aria-expanded
| Whether the element is expanded | |
aria-hidden
| Hides the element from the screen reader | |
aria-disabled
| Indicates a disabled element |

Focus management

In SPA (Single Page Application) applications, managing keyboard focus is critical. When a user navigates with the keyboard, focus must move in a logical order:

1import { useRef, useEffect } from 'react';
2
3function MissionModal({ isOpen, onClose, title, children }) {
4  const modalRef = useRef(null);
5  const closeButtonRef = useRef(null);
6
7  // Move focus to the modal after opening
8  useEffect(() => {
9    if (isOpen && closeButtonRef.current) {
10      closeButtonRef.current.focus();
11    }
12  }, [isOpen]);
13
14  // Focus trap - Tab doesn't leave the modal
15  const handleKeyDown = (e) => {
16    if (e.key === 'Escape') {
17      onClose();
18    }
19
20    if (e.key === 'Tab') {
21      const focusableElements = modalRef.current.querySelectorAll(
22        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
23      );
24      const firstElement = focusableElements[0];
25      const lastElement = focusableElements[focusableElements.length - 1];
26
27      if (e.shiftKey && document.activeElement === firstElement) {
28        e.preventDefault();
29        lastElement.focus();
30      } else if (!e.shiftKey && document.activeElement === lastElement) {
31        e.preventDefault();
32        firstElement.focus();
33      }
34    }
35  };
36
37  if (!isOpen) return null;
38
39  return (
40    <div
41      className="modal-overlay"
42      role="dialog"
43      aria-modal="true"
44      aria-labelledby="modal-title"
45      ref={modalRef}
46      onKeyDown={handleKeyDown}
47    >
48      <div className="modal-content">
49        <h2 id="modal-title">{title}</h2>
50        {children}
51        <button ref={closeButtonRef} onClick={onClose}>
52          Close
53        </button>
54      </div>
55    </div>
56  );
57}

Keyboard navigation

Every interactive element must be accessible from the keyboard. React supports key handling through

onKeyDown
,
onKeyUp
, and
onKeyPress
:

1function SpaceshipSelector({ ships, onSelect }) {
2  const [activeIndex, setActiveIndex] = useState(0);
3
4  const handleKeyDown = (e) => {
5    switch (e.key) {
6      case 'ArrowDown':
7        e.preventDefault();
8        setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
9        break;
10      case 'ArrowUp':
11        e.preventDefault();
12        setActiveIndex(prev => Math.max(prev - 1, 0));
13        break;
14      case 'Enter':
15      case ' ':
16        e.preventDefault();
17        onSelect(ships[activeIndex]);
18        break;
19    }
20  };
21
22  return (
23    <ul
24      role="listbox"
25      aria-label="Select a spaceship"
26      tabIndex={0}
27      onKeyDown={handleKeyDown}
28    >
29      {ships.map((ship, index) => (
30        <li
31          key={ship.id}
32          role="option"
33          aria-selected={index === activeIndex}
34          onClick={() => onSelect(ship)}
35          style={{
36            background: index === activeIndex ? '#1a3a5c' : 'transparent',
37          }}
38        >
39          {ship.name} - {ship.type}
40        </li>
41      ))}
42    </ul>
43  );
44}

Announcing dynamic changes (Live Regions)

When page content changes dynamically (e.g., a notification appears), the screen reader must be informed. We use

aria-live
:

1function MissionStatusBoard() {
2  const [status, setStatus] = useState('Awaiting launch');
3  const [logs, setLogs] = useState([]);
4
5  const updateStatus = (newStatus) => {
6    setStatus(newStatus);
7    setLogs(prev => [...prev, newStatus]);
8  };
9
10  return (
11    <div>
12      {/* aria-live="polite" - waits for the reader to finish current text */}
13      <div aria-live="polite" aria-atomic="true">
14        <h2>Mission status: {status}</h2>
15      </div>
16
17      {/* aria-live="assertive" - interrupts current reading */}
18      <div aria-live="assertive" className="sr-only">
19        {logs.length > 0 && logs[logs.length - 1]}
20      </div>
21
22      <button onClick={() => updateStatus('Countdown started')}>
23        Start countdown
24      </button>
25      <button onClick={() => updateStatus('Launch!')}>
26        Launch
27      </button>
28    </div>
29  );
30}

The sr-only class (screen reader only)

Sometimes we need text that is visible only to screen readers:

1// CSS style to visually hide, but not from screen readers
2const srOnlyStyle = {
3  position: 'absolute',
4  width: '1px',
5  height: '1px',
6  padding: 0,
7  margin: '-1px',
8  overflow: 'hidden',
9  clip: 'rect(0, 0, 0, 0)',
10  whiteSpace: 'nowrap',
11  borderWidth: 0,
12};
13
14function IconButton({ icon, label, onClick }) {
15  return (
16    <button onClick={onClick} aria-label={label}>
17      <span aria-hidden="true">{icon}</span>
18      <span style={srOnlyStyle}>{label}</span>
19    </button>
20  );
21}
22
23// Usage
24<IconButton icon="🚀" label="Start mission" onClick={startMission} />

Accessible forms

Forms are one of the most important areas of accessibility. Every field must have a label, and errors must be clearly communicated:

1function CrewRegistrationForm() {
2  const [errors, setErrors] = useState({});
3
4  return (
5    <form aria-label="Crew member registration">
6      <div>
7        <label htmlFor="crew-name">Full name:</label>
8        <input
9          id="crew-name"
10          type="text"
11          aria-required="true"
12          aria-invalid={!!errors.name}
13          aria-describedby={errors.name ? 'name-error' : undefined}
14        />
15        {errors.name && (
16          <span id="name-error" role="alert" style={{ color: 'red' }}>
17            {errors.name}
18          </span>
19        )}
20      </div>
21
22      <fieldset>
23        <legend>Specialization:</legend>
24        <label>
25          <input type="radio" name="role" value="pilot" />
26          Pilot
27        </label>
28        <label>
29          <input type="radio" name="role" value="engineer" />
30          Engineer
31        </label>
32        <label>
33          <input type="radio" name="role" value="scientist" />
34          Scientist
35        </label>
36      </fieldset>
37
38      <button type="submit">Join the crew</button>
39    </form>
40  );
41}

Summary

Accessibility in React is not an add-on, but an integral part of the interface creation process:

  • Semantic HTML is the foundation - use
    <nav>
    ,
    <main>
    ,
    <button>
    instead of
    <div>
    for everything
  • ARIA attributes supplement semantics when native elements are not enough
  • Focus management is critical in SPAs - a modal must "trap" focus, navigation must be logical
  • Keyboard navigation - every interactive element must be accessible without a mouse
  • Live regions (
    aria-live
    ) announce dynamic content changes
  • Forms require labels, error descriptions, and appropriate roles

Just as space station systems must be designed so that every crew member can use them under all conditions - even when the lights go out and the touchscreen stops working - an accessible React application ensures that every user can effectively use it.

Go to CodeWorlds