Семантика для screen readers
Screen reader (экранный диктор) -- программа, которая читает содержимое страницы вслух, опираясь на семантику HTML. Правильная разметка обеспечивает полноценный доступ к контенту для пользователей с нарушением зрения.
Зачем нужно
Screen reader-ы (NVDA, JAWS, VoiceOver, TalkBack) -- основной инструмент незрячих пользователей. Они не «видят» страницу, а строят Accessibility Tree из HTML. Если разметка семантическая -- пользователь может перемещаться по заголовкам, спискам, формам, landmarks. Если всё на <div> -- страница превращается в непрерывный поток текста.
Где используется
- Любой публичный сайт (юридическое требование во многих странах)
- Государственные и образовательные ресурсы (WCAG 2.1 AA)
- E-commerce и банковские приложения
- SPA-приложения с динамическим контентом
Предпосылки
Как screen reader воспринимает страницу
Accessibility Tree
Браузер строит Accessibility Tree из DOM:
DOM:
<nav aria-label="Основная">
<ul>
<li><a href="/">Главная</a></li>
<li><a href="/about" aria-current="page">О нас</a></li>
</ul>
</nav>
Accessibility Tree:
navigation "Основная"
list (2 items)
listitem
link "Главная"
listitem
link "О нас" (current page)
Screen reader озвучит: «Навигация, Основная. Список, 2 элемента. Ссылка, Главная. Ссылка, О нас, текущая страница.»
Landmarks -- ориентиры страницы
Landmarks позволяют screen reader-у быстро переключаться между секциями страницы:
<header> <!-- banner -->
<nav> <!-- navigation -->
...
</nav>
</header>
<main> <!-- main -->
<section> <!-- region (с aria-label) -->
<h1>...</h1>
</section>
<aside> <!-- complementary -->
...
</aside>
</main>
<footer> <!-- contentinfo -->
...
</footer>
Быстрая навигация по landmarks (VoiceOver: Rotor → Landmarks):
- banner (header)
- navigation (nav)
- main (main)
- complementary (aside)
- contentinfo (footer)
Именование landmarks
Если на странице несколько <nav>, каждый должен иметь уникальное имя:
<!-- Две навигации -- обе нужно именовать -->
<nav aria-label="Основная навигация">
<ul>...</ul>
</nav>
<nav aria-label="Подвал сайта">
<ul>...</ul>
</nav>
Заголовки -- навигация по структуре
Screen reader-ы позволяют перемещаться по заголовкам (H → следующий заголовок). Правильная иерархия критична:
<!-- Правильно: логическая иерархия -->
<h1>Магазин электроники</h1>
<h2>Смартфоны</h2>
<h3>Apple</h3>
<h3>Samsung</h3>
<h2>Ноутбуки</h2>
<h3>Игровые</h3>
<h3>Для работы</h3>
<!-- Неправильно: пропуск уровней -->
<h1>Магазин</h1>
<h4>Смартфоны</h4> <!-- Пропущены h2, h3 -->
<h2>Ноутбуки</h2>
Live regions -- динамические обновления
Screen reader не замечает DOM-изменения без live regions:
<!-- Уведомление при сохранении формы -->
<div role="status" aria-live="polite" id="form-status"></div>
<!-- Ошибка -- немедленно объявить -->
<div role="alert" aria-live="assertive" id="error-msg"></div>
<script>
async function saveForm() {
try {
await fetch('/api/save', { method: 'POST', body: formData });
document.getElementById('form-status').textContent = 'Данные сохранены';
// SR озвучит: "Данные сохранены" (после текущей фразы)
} catch (e) {
document.getElementById('error-msg').textContent = 'Ошибка сохранения';
// SR озвучит: "Ошибка сохранения" (немедленно)
}
}
</script>
aria-atomic и aria-relevant
<!-- aria-atomic="true": озвучить весь блок при любом изменении -->
<div aria-live="polite" aria-atomic="true" id="cart-total">
Итого: <span id="price">0</span> руб.
</div>
<!-- При обновлении #price SR скажет: "Итого: 1500 руб." (весь блок) -->
<!-- aria-relevant: что отслеживать -->
<ul aria-live="polite" aria-relevant="additions removals">
<!-- SR объявит добавление и удаление элементов -->
</ul>
Visually Hidden -- скрыть визуально, оставить для SR
/* Класс для визуального скрытия (доступен screen reader-у) */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
<!-- Кнопка с иконкой и скрытым текстом -->
<button>
<svg aria-hidden="true"><!-- иконка корзины --></svg>
<span class="visually-hidden">Добавить в корзину</span>
</button>
<!-- Skip link (первый элемент страницы) -->
<a href="#main-content" class="visually-hidden">
Перейти к основному содержимому
</a>
<!-- Дополнительный контекст для SR -->
<table>
<caption class="visually-hidden">Цены на тарифы</caption>
...
</table>
Не путать с display: none и visibility: hidden -- они скрывают элемент и от screen reader.
Skip links -- пропуск навигации
<body>
<!-- Первый элемент -- skip link -->
<a href="#main" class="skip-link">Перейти к содержимому</a>
<header>
<nav><!-- 20 ссылок навигации --></nav>
</header>
<main id="main" tabindex="-1">
<!-- Основной контент -->
</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 1000;
padding: 8px 16px;
background: #000;
color: #fff;
}
/* Показать при фокусе (Tab) */
.skip-link:focus {
top: 0;
}
Формы и screen readers
<form>
<!-- Группировка: fieldset + legend -->
<fieldset>
<legend>Контактная информация</legend>
<!-- SR: "Контактная информация, группа" -->
<!-- label привязан к input -->
<label for="name">Имя:</label>
<input type="text" id="name" required
aria-describedby="name-hint">
<p id="name-hint">Как к вам обращаться</p>
<!-- SR: "Имя, обязательное текстовое поле. Как к вам обращаться" -->
<label for="email">Email:</label>
<input type="email" id="email" required
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" role="alert">Введите корректный email</p>
<!-- SR: "Email, обязательное, невалидное. Введите корректный email" -->
</fieldset>
<button type="submit">Отправить</button>
</form>
Изображения и медиа
<!-- Информативное изображение -->
<img src="chart.png" alt="График продаж: рост 25% за Q3 2025">
<!-- Декоративное изображение -->
<img src="divider.png" alt="" role="presentation">
<!-- Сложное изображение -->
<figure>
<img src="diagram.png" alt="Архитектура микросервисов"
aria-describedby="diagram-desc">
<figcaption id="diagram-desc">
Схема показывает 5 сервисов, связанных через API Gateway...
</figcaption>
</figure>
<!-- Видео с субтитрами -->
<video controls>
<source src="tutorial.mp4" type="video/mp4">
<track kind="captions" src="captions-ru.vtt" srclang="ru" label="Русские">
</video>
Тестирование с screen readers
| Screen Reader | ОС | Бесплатный | Браузер |
|---|---|---|---|
| NVDA | Windows | Да | Firefox, Chrome |
| JAWS | Windows | Нет | Chrome, Edge |
| VoiceOver | macOS / iOS | Да (встроен) | Safari |
| TalkBack | Android | Да (встроен) | Chrome |
Быстрое тестирование
1. NVDA (Windows): скачать с nvaccess.org → Insert+↓ (читать всё)
2. VoiceOver (Mac): Cmd+F5 → VO+→ (следующий элемент)
3. Chrome DevTools: Elements → Accessibility panel → Accessibility Tree
4. Расширение axe DevTools: запустить аудит
Частые ошибки
| Ошибка | Проблема для SR | Решение |
|---|---|---|
<div> вместо <button> |
SR не объявляет как кнопку | Нативный <button> |
Нет alt на <img> |
SR читает имя файла | Добавить описательный alt |
display: none для «скрытого» текста |
SR не увидит | .visually-hidden класс |
| Нет landmarks | Нет быстрой навигации | <header>, <main>, <nav> |
| Динамический контент без live region | SR не замечает изменения | aria-live |
| Пропуск уровней заголовков | Сломанная иерархия | h1 → h2 → h3 последовательно |
Практика
- Включи NVDA или VoiceOver и проведи 10 минут на своём сайте
- Добавь skip link как первый элемент страницы
- Проверь все изображения -- у каждого должен быть корректный
alt - Добавь
aria-liveregion для уведомлений - Проверь landmarks через Chrome Accessibility Tree
Связанные темы
- ARIA атрибуты -- дополнительная семантика
- Фокус и клавиатурная навигация -- клавиатурный доступ
- Семантическая разметка -- основа доступности
- Формы в HTML -- доступные формы
- Заголовки h1-h6 -- иерархия заголовков