Доступность: сайт для всех
Доступность (A11y): семантика, клавиатурная навигация, видимый фокус, ARIA, контраст и анимации для пользователей с разными возможностями.
1. Что такое доступность
Доступность (англ. accessibility, сокращённо a11y — «11 букв между a и y») — это возможность использовать сайт всем людям, независимо от ограничений: незрячим и слабовидящим, людям с нарушениями моторики, слуха, а также пожилым и тем, кто просто пользуется сайтом с клавиатуры, телефона или при плохом сетевом соединении.
Доступность выгодна не только людям с инвалидностью: она улучшает сайт для всех. Хорошая контрастность помогает при ярком солнце, понятная клавиатурная навигация — когда нет мыши, а осмысленная семантика — поисковикам и голосовым ассистентам.
- семантика и структура — страница понятна без визуального оформления;
- клавиатура — на каждую ссылку и кнопку можно попасть с клавиатуры;
- контраст и размер — текст читается при любом зрении;
- описания — изображения и мультимедиа имеют текстовую альтернативу;
- осторожность с анимациями — движение не вызывает дискомфорт.
2. Семантика — скелет доступности
Семантические теги дают скринридеру и браузеру понять структуру страницы: где навигация, где основное содержание, что является кнопкой, а что просто текст. Невидимые программы не смотрят на страницу — они читают HTML.
<div class="header"> <span class="btn">Главная</span> <!-- нельзя -->
<header> <button>Главная</button> <!-- правильно -->Базовый скелет доступной страницы использует теги header, nav, main, aside, footer и один элемент main на страницу. Скринридер по этим тегам предлагает пользователю быструю навигацию: «перейти к содержанию», «перейти к навигации».
- nav — одна или несколько навигаций (группы ссылок по сайту);
- main — уникальное основное содержимое (не повторяется на других страницах);
- button, a, input — интерактивные элементы, а не div с обработчиком клика;
- ul/ol — списки, а не строки с разделителями «•»;
- h1–h6 — только заголовки, не элемент для размера шрифта.
div с обработчиком клика вместо button — самая частая ошибка: скринридер не знает, что элемент нажимается, а клавиатура не может нажать Enter. Если нужен нестандартный внешний вид кнопки — стилизуйте настоящий button.3. Заголовки и порядок чтения
Заголовки — это содержание страницы для скринридера. Пользователь открывает список заголовков и перемещается по ним, как по оглавлению. Поэтому важна не только семантика, но и осмысленная иерархия: h1 один на страницу, дальше уровни не прыгают через один.
<h1>Каталог</h1> <!-- единственный h1 -->
<h2>Ноутбуки</h2>
<h3>Игровые</h3>
<h3>Для работы</h3>
<h2>Мониторы</h2>- уровни не пропускаются: h1 → h4 без h2/h3 путает ориентир;
- размер шрифта задаётся CSS, а не выбором тега h (h3 с текстом 28px — допустимо, если это реально третий уровень);
- заголовок отражает содержимое блока и не является просто словом «Ещё».
4. Ссылки и кнопки
Имя ссылки и кнопки — это её текст или aria-label. Имя должно быть осмысленным отдельно от контекста: «подробнее» и «читать дальше» повторены сто раз на странице — бесполезные для пользователя скринридера ориентиры, ему приходится открывать каждую, чтобы понять, куда ведёт.
<a href="catalog/iphone-15.html">Подробнее об iPhone 15</a>
<a href="catalog/airpods.html">Подробнее об AirPods</a>
<!-- вместо: <a href="...">Подробнее</a> и <a href="...">Подробнее</a> -->- текст ссылки описывает цель, а не «подробнее/здесь/клик»;
- разные страницы одного типа — разные тексты ссылок;
- кнопка, у которой открывается окно, сообщает об этом через aria-haspopup;
- если визуально это кнопка — это
button, если переход на страницу —aс атрибутом href.
5. Клавиатура и фокус
Каждый интерактивный элемент должен получать фокус с клавиатуры (Tab), а действия выполняться по Enter/Space. Нативные элементы этой логике уже следуют — ещё один довод не изобретать кнопки из div.
a:focus, button:focus, input:focus {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* не надо: outline: none; без замены на видимый индикатор */- видимый фокус (outline) обязателен — никогда не убирайте его совсем;
:focus-visibleпоказывает кольцо только для клавиатуры, не для клика мышью — самый современный вариант;- не ломайте порядок Tab:
tabindexбольше 0 переопределяет естественный порядок — избегайте; - родитель с
tabindexот 1 до 0 иaria-hidden— тонкости для виджетов, не нужные на старте.
6. Скринридер: скрытие и описания
Иногда элемент нужен визуально, но не нужен скринридеру (декоративная иконка), а иногда — наоборот: текст существует только для скринридера (подпись «Пропустить меню»). Двум ситуациям отвечают aria-hidden и класс sr-only.
<!-- декоративную иконку скрываем от скринридера -->
<button>
<svg aria-hidden="true" class="icon">…</svg>
Удалить
</button>
/* текст, видимый только скринридеру */
.sr-only {
position: absolute;
width: 1px; height: 1px;
clip-path: inset(50%);
margin: -1px;
overflow: hidden;
}aria-hidden="true"прячет элемент и всех его детей от скринридера;- скрытый
display: noneэлемент пропадает и визуально, и из дерева доступности — это ок; - класс sr-only оставляет текст визуально невидимым, но доступным скринридеру;
- у
svg-иконки без текста обязательно либо aria-hidden, либо role="img" + aria-label.
aria-hidden="true" на фокусируемый элемент: невидимый для скринридера фокус переводит пользователя «в никуда». aria-hidden допустимо только на недекоративное статичное содержимое.7. ARIA: роль, имя, состояние
ARIA-атрибуты исправляют то, что не выражается нативными тегами: роль элемента, его имя и текущее состояние. Главное правило: без ARIA, если можно нативным тегом — нативные элементы уже поддерживаются скринридерами одинаково хорошо.
<!-- состояние раскрывающегося меню -->
<button aria-expanded="false" aria-controls="menu">Разделы</button>
<ul id="menu" hidden>…</ul>
<!-- роль с именем для svg-диаграммы -->
<svg role="img" aria-label="Продажи в 2025 году: 120 млн руб.">…</svg>aria-labelзадаёт имя элементу без видимого текста;aria-expandedсообщает о раскрытом состоянии;aria-describedbyпривязывает поясняющий текст (подсказку поля ввода);aria-liveозвучивает динамические изменения (счётчик корзины, статус отправки).
8. Формы: подписи и ошибки
Каждое поле должно иметь текстовую подпись, связанную с ним: скринридер объявит «Имя: редактируемое поле» при фокусе. Связь делается через label for и id, либо обёрткой поля в label.
<label for="email">Электронная почта</label>
<input type="email" id="email" name="email" required>
<label for="password">Пароль</label>
<input type="password" id="password" name="password"
autocomplete="current-password">Сообщение об ошибке должно быть не только цветным: цвет не справится для незрячего и дальтоника. Свяжите текст ошибки с полем через aria-describedby, а невалидное состояние обозначьте aria-invalid.
<label for="phone">Телефон</label>
<input type="tel" id="phone" name="phone"
aria-describedby="phone-err" aria-invalid="true">
<span id="phone-err" class="error">Формат: +7 900 000-00-00</span>- placeholder не заменяет подпись — после ввода текста он исчезает;
- ошибки появляются рядом с полем и связаны aria-describedby;
- сообщение после отправки формы читается через aria-live="assertive";
- форма не блокирует отправку без объяснения — правило: «лучше пояснить, чем отвергнуть».
9. Контраст и размер текста
Текст должен читаться при любых условиях. WCAG 2.1 задаёт два порога: не менее 4.5:1 для обычного текста и 3:1 для крупного (от 24px или 18.66px жирного). На практике берите 4.5:1 как рабочее правило и проверяйте светлые цвета на фоне.
| Контраст | Где применяется |
|---|---|
| 4.5:1 и выше | весь обычный текст и интерфейс |
| 3:1 и выше | крупный текст (24px+), интерфейсы и объекты |
| 1.4.4 текст | размер не фиксируется, страница читается при увеличении до 200% |
/* серый текст на белом: проверьте, а не красивый ли он у вас случайно */
body { color: #1f2937; } /* #1f2937 на #ffffff = ~16:1 */
.muted { color: #6b7280; } /* #6b7280 на #ffffff = ~4.4:1 */
.bad { color: #9ca3af; } /* ~2.8:1 — уже не проходит */- используйте для контраста онлайн-калькуляторы (WebAIM contrast checker);
- не передавайте смысл только цветом: иконка + подпись, а не просто красная «✗»;
- допустимый минимум размера основного текста — 16px (1rem);
- при увеличении страницы до 200% контент не должен пропадать.
10. Изображения и мультимедиа
У каждого изображения со смыслом есть альтернативный текст alt: скринридер читает его вместо картинки. Пустой alt у декоративных картинок, а не отсутствие атрибута вообще — отсутствие заставляет читать имя файла.
<!-- полезное фото: описываем содержание -->
<img src="team.jpg" alt="Команда на тимбилдинге в горах">
<!-- декоративная картинка: ничего не значит для смысла -->
<img src="decoration.svg" alt="" class="hero-decor">
<!-- iframe-видео: подпись и альтернатива -->
<iframe title="Демо: как собрать лендинг" src="https://…"></iframe>altописывает содержание изображения, а не повторяет подпись;- декор —
alt="", informative — осмысленный alt; videoс аудиодорожкой требует субтитров (<track kind="captions">);- для
iframeнужен атрибут title.
11. Анимации и motion
Движение, параллакс и вспышки вызывают у части людей тошноту, головокружение и даже приступы. Пользователи с prefers-reduced-motion: reduce просят систему: «не двигай». Веб должен это уважать.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}- декоративные анимации отключаем при prefers-reduced-motion: reduce;
- важное сообщение не должно мигать дольше трёх раз в секунду (SC 2.3.1);
- параллакс, автоскролл и крупные transform-и — с отключаемым вариантом.
12. Проверка и чек-лист
Доступность проверяется снаружи: инструменты находят грубые ошибки, а человек — часть барьеров. Работайте в цикле: автоматическая проверка → ручная с клавиатурой → тест со скринридером.
- Lighthouse (Chrome DevTools) — быстрая автоматическая проверка и оценка;
- axe DevTools / WAVE — список конкретных ошибок с локацией;
- проход по странице только с клавиатуры: весь функционал достигаем Tab и Enter;
- скринридер: пользуюсь естественно, называет поля и состояния;
- контраст всех текстов ≥ 4.5:1, а смысл не передаётся только цветом.
Минимальный чек-лист до публикации
- главная навигация и содержимое помечены
<nav>и<main>; - h1 один, уровни заголовков не прыгают;
- все кликабельные «кнопки» — настоящие button, а ссылки имеют href;
- видимый outline для focus у всех интерактивных элементов;
- у каждого поля формы есть label, ошибки связаны с полем;
- изображения имеют alt или пустой alt, видео — субтитры;
- контраст текста ≥ 4.5:1, анимации уважают prefers-reduced-motion.
Что дальше
Курс завершён.