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.
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:
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 (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 |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}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}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}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} />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}Accessibility in React is not an add-on, but an integral part of the interface creation process:
<nav>, <main>, <button> instead of <div> for everythingaria-live) announce dynamic content changesJust 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.