Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Доступность (Accessibility) в веб-разработке
Доступность (A11y, от letters a и y с 11 буквами между ними) — это практика создания веб-контента, доступного для всех людей, включая людей с инвалидностью. Это правовое, этическое и экономическое требование.
Кто нуждается в доступности?
- Люди с нарушением зрения (слепые, слабовидящие) — используют скрин-ридеры
- Люди с нарушением слуха — нужны субтитры и визуальные индикаторы
- Люди с нарушением двигательных функций — навигация только клавиатурой
- Люди с когнитивными нарушениями — простая и понятная структура
- Люди с заболеваниями глаз — контрастность, размер текста
- Пожилые люди — просто полезно для всех
1. Семантичный HTML
Основная основа доступности — использование правильных HTML элементов.
Плохо: таблица вёрстана на divs
<!-- Неправильно -->
<div class="button" onclick="submitForm()">Submit</div>
<div>Page Title</div>
<div>Content goes here</div>
<!-- Правильно -->
<button type="submit">Submit</button>
<h1>Page Title</h1>
<main>
<article>
Content goes here
</article>
</main>
Скрин-ридер понимает семантику:
<!-- Скрин-ридер скажет "heading level 1: Page Title" -->
<h1>Page Title</h1>
<!-- Скрин-ридер скажет "button: Submit" -->
<button type="submit">Submit</button>
<!-- Скрин-ридер скажет "link: Click here" -->
<a href="/page">Click here</a>
Правильная иерархия заголовков
<!-- Правильно -->
<h1>Main Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
<!-- Неправильно: пропускаем уровни -->
<h1>Main Title</h1>
<h3>Subsection (нет h2!)</h3>
2. ARIA атрибуты (Accessible Rich Internet Applications)
ARIA помогает когда семантического HTML недостаточно.
aria-label: описание для элемента
<!-- Кнопка без видимого текста -->
<button aria-label="Close dialog">✕</button>
<!-- Скрин-ридер скажет: "button: Close dialog" -->
aria-labelledby: связь с другим элементом
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure?</p>
<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<!-- диалог -->
</div>
aria-hidden: скрыть от скрин-ридера
<!-- Декоративный элемент, не нужно читать -->
<span aria-hidden="true">❤</span>
<!-- Но если это важно, не используй aria-hidden -->
<span aria-hidden="true">✕</span> <!-- Плохо, нужно читать -->
<button aria-label="Close">✕</button> <!-- Хорошо -->
aria-live: уведомления об изменениях
<!-- Скрин-ридер объявит изменение в реальном времени -->
<div aria-live="polite" aria-atomic="true">
Items in cart: 5
</div>
<!-- Приоритет: polite (дождаться паузы) vs assertive (немедленно) -->
aria-expanded: состояние (открыто/закрыто)
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<nav id="menu" hidden>
<ul>
<li><a href="/home">Home</a></li>
</ul>
</nav>
<!-- При клике обновляем aria-expanded -->
button.setAttribute("aria-expanded", "true");
menu.hidden = false;
3. Клавиатурная навигация
Многие люди не используют мышь.
Focusable элементы
<!-- Интерактивные элементы должны быть фокусируемые -->
<button>Submit</button> <!-- Фокусируем по умолчанию -->
<a href="/">Link</a> <!-- Фокусируем по умолчанию -->
<input type="text" /> <!-- Фокусируем по умолчанию -->
<!-- Если не интерактивно, но нужно быть фокусируемым -->
<div tabindex="0">Focusable div</div> <!-- tabindex=0: в tab order -->
<div tabindex="-1">Not in tab order, but can focus programmatically</div>
Порядок табуляции
<!-- Плохо: нелогичный порядок -->
<input type="text" tabindex="3" />
<input type="text" tabindex="1" />
<input type="text" tabindex="2" />
<!-- Хорошо: используй семантический порядок в HTML -->
<input type="text" /> <!-- tabindex не нужен, порядок из HTML -->
<input type="text" />
<input type="text" />
Видимый focus
/* Никогда не удаляй focus outline без замены! */
/* Плохо -->
button:focus { outline: none; }
/* Хорошо -->
button:focus { outline: 3px solid blue; }
/* Или стильный focus -->
button:focus-visible {
outline: 2px solid #4CAF50;
outline-offset: 2px;
}
4. Контрастность цветов
Должна быть достаточная контрастность для людей с нарушением зрения.
WCAG стандарты контрастности
- AA уровень: минимум 4.5:1 для текста, 3:1 для крупного текста
- AAA уровень: минимум 7:1 для текста, 4.5:1 для крупного текста
/* Хорошая контрастность -->
.text {
color: #000000; /* чёрный -->
background-color: #ffffff; /* белый -->
/* Контраст: 21:1 - идеально -->
}
/* Плохая контрастность -->
.text {
color: #777777; /* серый -->
background-color: #888888; /* тёмно-серый -->
/* Контраст: ~1.1:1 - очень плохо -->
}
Проверить контрастность: WebAIM Contrast Checker
5. Текстовые альтернативы
alt текст для изображений
<!-- Плохо: пустой alt или неинформативный -->
<img src="photo.jpg" alt="" />
<img src="photo.jpg" alt="image" />
<!-- Хорошо: описывает содержимое -->
<img src="team-photo.jpg" alt="Team of 5 engineers standing in office" />
<!-- Для декоративных изображений -->
<img src="decoration.jpg" alt="" /> <!-- пустой alt -->
<img src="decoration.jpg" aria-hidden="true" /> <!-- или aria-hidden -->
Субтитры и расшифровки
<!-- Видео с субтитрами -->
<video width="400" height="300" controls>
<source src="video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" />
</video>
<!-- Аудио с расшифровкой -->
<audio controls>
<source src="podcast.mp3" type="audio/mpeg" />
</audio>
<p><a href="transcript.txt">Read transcript</a></p>
6. React компоненты с доступностью
// components/Modal.tsx
interface ModalProps {
isOpen: boolean;
title: string;
onClose: () => void;
children: React.ReactNode;
}
export function Modal({ isOpen, title, onClose, children }: ModalProps) {
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(e) => e.currentTarget === e.target && onClose()}
>
<div className="modal-content">
<button
aria-label="Close dialog"
onClick={onClose}
className="close-btn"
>
✕
</button>
<h2 id="modal-title">{title}</h2>
{children}
</div>
</div>
);
}
// components/Dropdown.tsx
export function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
setIsOpen(false);
buttonRef.current?.focus();
}
};
if (isOpen) {
document.addEventListener("keydown", handleKeyDown);
}
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen]);
return (
<div>
<button
ref={buttonRef}
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Options
</button>
{isOpen && (
<nav id="dropdown-menu" ref={menuRef}>
<ul>
<li><a href="#">Option 1</a></li>
<li><a href="#">Option 2</a></li>
</ul>
</nav>
)}
</div>
);
}
7. Тестирование доступности
Инструменты
- axe DevTools — браузерное расширение для проверки WCAG
- WAVE — WebAIM инструмент для визуализации проблем
- Lighthouse — встроено в Chrome DevTools
- jest-axe — для unit тестов доступности
// Пример теста доступности
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
export {}; // Для TypeScript
expect.extend(toHaveNoViolations);
it("should have no accessibility violations", async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Ручное тестирование
1. Отключи мышь, навигируй только клавиатурой (Tab, Enter, Arrows)
2. Используй скрин-ридер (NVDA, JAWS, VoiceOver на Mac)
3. Увеличь текст до 200%
4. Проверь контрастность
5. Отключи JS и проверь базовую функциональность
8. Практический чеклист
- Семантичный HTML (h1-h6, nav, main, article, button, link)
- alt текст на всех значащих изображениях
- Все интерактивные элементы фокусируемы (Tab работает)
- Focus видимый (outline или другой стиль)
- Контрастность минимум 4.5:1 для текста
- ARIA labels где нужно (button без текста, иконки)
- Форма с label для каждого input
- Ошибки форм понятны и легко исправляются
- Видео с субтитрами
- Никаких миганий > 3 раз в секунду
- Тесты с jest-axe или axe DevTools
Вывод
Доступность — это не опция, это requirement. Хороший фронтендер учитывает a11y с начала проекта, не в конце. Это не сложно, если следовать стандартам WCAG и использовать семантичный HTML. В итоге все выигрывают: люди с инвалидностью получают доступ, юзеры с хорошим зрением получают лучший контент структуру, SEO улучшается.