Na stacji kosmicznej każdy członek załogi musi mieć dostęp do systemów sterowania - niezależnie od tego, czy obsługuje je wzrokiem, dotykiem czy komendami głosowymi. Dokładnie tak samo działa dostępność (accessibility, w skrócie a11y) w aplikacjach webowych. Budując interfejs w React, musimy zadbać o to, by każdy użytkownik mógł z niego korzystać - osoby niewidome używające czytników ekranu, osoby poruszające się tylko klawiaturą, czy osoby z zaburzeniami widzenia kolorów.
Według WHO, ponad miliard ludzi na świecie żyje z jakąś formą niepełnosprawności. To oznacza, że aplikacja niedostępna wyklucza ogromną grupę potencjalnych użytkowników. Poza aspektem etycznym, dostępność:
Fundamentem dostępności jest używanie właściwych elementów HTML. React zachęca do tworzenia komponentów, ale łatwo wpaść w pułapkę nadużywania
<div>:1// ZLE - "div soup" - czytnik ekranu nie rozumie struktury
2function BadNavigation() {
3 return (
4 <div className="nav">
5 <div className="nav-item" onClick={goHome}>Start</div>
6 <div className="nav-item" onClick={goMissions}>Misje</div>
7 <div className="nav-item" onClick={goCrewPanel}>Załoga</div>
8 </div>
9 );
10}
11
12// DOBRZE - semantyczne elementy HTML
13function GoodNavigation() {
14 return (
15 <nav aria-label="Menu główne stacji">
16 <ul>
17 <li><a href="/">Start</a></li>
18 <li><a href="/missions">Misje</a></li>
19 <li><a href="/crew">Załoga</a></li>
20 </ul>
21 </nav>
22 );
23}Kluczowe semantyczne elementy to:
<nav>, <main>, <header>, <footer>, <section>, <article>, <aside>, <button>, <form>, <label>.ARIA (Accessible Rich Internet Applications) to zbiór atrybutów, które dodają informacje o roli i stanie elementów dla technologii asystujących. W React używamy ich w formacie camelCase lub z myślnikami:
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">Szukaj galaktyk:</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}Najczęściej używane atrybuty ARIA w React:
| Atrybut | Zastosowanie | |---------|-------------| |
role | Definiuje rolę elementu (alert, dialog, tabpanel) |
| aria-label | Tekstowa etykieta dla elementu |
| aria-labelledby | ID elementu, który jest etykietą |
| aria-describedby | ID elementu z dodatkowym opisem |
| aria-live | Ogłasza dynamiczne zmiany treści |
| aria-expanded | Czy element jest rozwinięty |
| aria-hidden | Ukrywa element przed czytnikiem |
| aria-disabled | Wskazuje element nieaktywny |W aplikacjach SPA (Single Page Application) zarządzanie fokusem klawiatury jest krytyczne. Gdy użytkownik nawiguje klawiaturą, fokus musi przesuwać się w logicznej kolejności:
1import { useRef, useEffect } from 'react';
2
3function MissionModal({ isOpen, onClose, title, children }) {
4 const modalRef = useRef(null);
5 const closeButtonRef = useRef(null);
6
7 // Przenieś fokus do modala po otwarciu
8 useEffect(() => {
9 if (isOpen && closeButtonRef.current) {
10 closeButtonRef.current.focus();
11 }
12 }, [isOpen]);
13
14 // Pułapka fokusa - Tab nie wychodzi poza 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 Zamknij
53 </button>
54 </div>
55 </div>
56 );
57}Każdy interaktywny element musi być dostępny z klawiatury. React wspiera obsługę klawiszy przez
onKeyDown, onKeyUp i 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="Wybierz statek kosmiczny"
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}Gdy treść strony zmienia się dynamicznie (np. pojawia się powiadomienie), czytnik ekranu musi o tym wiedzieć. Używamy
aria-live:1function MissionStatusBoard() {
2 const [status, setStatus] = useState('Oczekiwanie na start');
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" - czeka aż czytnik skończy bieżący tekst */}
13 <div aria-live="polite" aria-atomic="true">
14 <h2>Status misji: {status}</h2>
15 </div>
16
17 {/* aria-live="assertive" - przerywa bieżące czytanie */}
18 <div aria-live="assertive" className="sr-only">
19 {logs.length > 0 && logs[logs.length - 1]}
20 </div>
21
22 <button onClick={() => updateStatus('Odliczanie rozpoczęte')}>
23 Rozpocznij odliczanie
24 </button>
25 <button onClick={() => updateStatus('Start!')}>
26 Start
27 </button>
28 </div>
29 );
30}Czasami potrzebujemy tekstu widocznego tylko dla czytników ekranu:
1// Styl CSS do ukrycia wizualnego, ale nie dla czytników ekranu
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// Użycie
24<IconButton icon="🚀" label="Rozpocznij misję" onClick={startMission} />Formularze to jeden z najważniejszych obszarów dostępności. Każde pole musi mieć etykietę, a błędy muszą być jasno komunikowane:
1function CrewRegistrationForm() {
2 const [errors, setErrors] = useState({});
3
4 return (
5 <form aria-label="Rejestracja członka załogi">
6 <div>
7 <label htmlFor="crew-name">Imię i nazwisko:</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>Specjalizacja:</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 Inżynier
31 </label>
32 <label>
33 <input type="radio" name="role" value="scientist" />
34 Naukowiec
35 </label>
36 </fieldset>
37
38 <button type="submit">Dołącz do załogi</button>
39 </form>
40 );
41}Dostępność w React to nie dodatek, ale integralna część procesu tworzenia interfejsu:
<nav>, <main>, <button> zamiast <div> na wszystkoaria-live) ogłaszają dynamiczne zmiany treściPodobnie jak systemy stacji kosmicznej muszą być zaprojektowane tak, by każdy członek załogi mógł z nich korzystać w każdych warunkach - nawet gdy światła zgasną, a ekran dotykowy przestanie działać - tak dostępna aplikacja React zapewnia, że każdy użytkownik może skutecznie z niej korzystać.