lenec ru

← все посты

Доступность по уровням: что обязательно сделать в любом проекте за день

19K

За семь лет фронта я видела одну и ту же сцену много раз: на ретро поднимается тема доступности, кто-то говорит «нам нужен полноценный аудит WCAG AA», бэклог пухнет на три спринта вперёд, а через полгода ничего не сделано. Потому что задача в таком виде — слишком большая. Её нельзя «сделать», её можно только бесконечно делать.

В моей дизайн-системе мы пошли другим путём. Разделили доступность на три уровня по стоимости. Уровень 0 — базовое, что заводится за день силами одного фронта. Уровень 1 — то, что требует пары спринтов и согласования с продуктом. Уровень 2 — глубокий аудит с привлечением людей с инвалидностью. Эта статья — про уровень 0. То, что должно быть в любом проекте, который ты ведёшь.

Зачем это вообще нужно

У меня нет миссии «спасти мир для людей с инвалидностью». У меня есть прагматика. Базовая доступность даёт три вещи: интерфейс перестаёт ломаться у пользователей с клавиатурным управлением, страницы лучше работают с автотестами, и юристы перестают писать гневные письма. В России пока на это редко подают в суд, но в ЕС и США — регулярно.

Уровень 0 — это про то, чтобы сайтом можно было пользоваться без мыши и чтобы скринридер хотя бы не врал. Не идеал, но граница, ниже которой нельзя.

Чек-лист уровня 0

1. lang на html

Самое глупое и самое забываемое. Без атрибута lang на <html> скринридер не понимает, на каком языке читать. Английский голос на русском тексте — это пытка.

<html lang="ru">

В SPA на React/Vue это часто прибито к статичному index.html, и про него забывают, когда добавляют переключатель языка. Если у тебя multilang, обновляй атрибут на роутинге.

2. Title страницы должен меняться

В классическом MPA это бесплатно, в SPA — нет. Если у тебя пять разделов, и на всех в шапке одно и то же «Мой проект», скринридер на каждом переходе озвучивает одинаковое название. Пользователь теряется.

Решений два: либо react-helmet-async/@vueuse/head, либо ручное document.title = ... в эффекте на роуте. В Astro и Next.js всё уже встроено в head компонента страницы.

3. Видимый focus

Я писала отдельную статью про :focus-visible, но базовое правило коротко: не убирай outline. Если убрал — обязательно нарисуй замену. Самый глупый антипаттерн, который я встречала в продакшне:

*:focus { outline: none; }

Это значит, что человек, перемещающийся по странице через Tab, не видит, где он находится. Если такая строчка есть в проекте — удали её прямо сейчас, не дочитывая статью.

:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: 4px;
}

4. Кнопки — это button, ссылки — это a

Признак фронтендера, который ещё не разбирался с доступностью: на странице пять div с обработчиками клика. Они не работают с клавиатуры, не озвучиваются скринридером как интерактивные, и в них нельзя попасть Tab'ом.

Правило простое:

  • Действие на текущей странице (открыть модалку, переключить вкладку, отправить форму) — <button type="button">.
  • Переход на другой URL — <a href="...">.

Если очень хочется div, потому что «дизайнер сказал», — это плохой дизайнер. Можно взять button и через CSS сделать его внешне любым.

5. Все картинки имеют alt

Не «не забудь alt», а буквально каждая. Если картинка несёт смысл — описание этого смысла. Если декоративная — пустая строка alt="". Не отсутствие атрибута, а именно пустая строка. Без alt вообще скринридер озвучит имя файла: «slash-assets-slash-banner-final-final-version-two-png». Это позор.

<img src="/team.jpg" alt="Команда из шести разработчиков на ретроспективе">
<img src="/decoration-flourish.svg" alt="">

SVG как иконки — отдельная история. Если иконка только декоративная: aria-hidden="true". Если несёт смысл (типа «удалить» рядом с пустой кнопкой): role="img" и aria-label, либо рядом текст, доступный скринридеру через <span class="sr-only">.

6. Контраст для текста

4.5:1 для основного текста, 3:1 для крупного (от 18pt или 14pt жирного). Проверь все цвета, которые есть в проекте: основной текст, ссылки, плейсхолдеры в инпутах, бордеры активных состояний. Самое распространённое — серенький текст «hint» поверх белого фона с контрастом 2.8:1. Ставится дизайнером, проходит ревью, попадает в прод, а пожилой пользователь не может прочитать подсказки в форме регистрации.

Быстрая проверка — через DevTools. В Chrome открой инспектор, ткни в текст, в панели Styles рядом с color увидишь индикатор контрастности.

7. Skip link на главные блоки

Если у тебя в шапке двадцать ссылок, пользователь клавиатуры, который пришёл прочитать статью, должен пройти через все двадцать. Это бесит. Решение — невидимая ссылка в начале body, которая показывается на фокус:

<a href="#main" class="skip-link">Перейти к содержимому</a>
<header>...</header>
<main id="main">...</main>
.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  position: static;
  display: inline-block;
  padding: 8px 16px;
  background: var(--color-bg);
}

8. Формы: каждый input связан с label

Не плейсхолдер вместо лейбла. Не «как-то рядом текст лежит». Связь либо через for/id, либо обёрнутый input внутри label.

<label for="email">Email</label>
<input id="email" type="email" name="email">

Без этой связи скринридер не понимает, какой инпут что значит. Касание мобильного по тексту лейбла не фокусирует поле. Ошибки валидации — отдельная история, про неё уровень 1.

9. Heading-структура без дыр

На странице должен быть один h1. Дальше — иерархия без пропусков уровней. Если идёт h2, потом сразу h4, скринридер сообщает об ошибке структуры. Пользователь, который перемещается по заголовкам командой «следующий заголовок», теряется.

Часто разработчики используют h2-h6 ради размера шрифта. Это неправильно: семантический уровень и визуальный размер — разные вещи. Хочешь маленький заголовок, но второго уровня — пиши <h2 class="text-sm">.

10. Landmarks: header, main, nav, footer

Завернуть страницу в семантические теги — две минуты работы. Эффект — навигация скринридера через rotor (на VoiceOver — Ctrl+Option+U), которая позволяет прыгать по областям страницы.

<body>
  <header>...</header>
  <nav aria-label="Главная">...</nav>
  <main>...</main>
  <footer>...</footer>
</body>

Если на странице несколько nav (главная и в подвале) — у каждого должен быть свой aria-label. Иначе скринридер их не различит.

Как этого добиться за день

В небольшом проекте этот чек-лист действительно проходится за рабочий день. Я обычно делаю так:

  1. Утром — линтеры. Установить eslint-plugin-jsx-a11y для React или eslint-plugin-vuejs-accessibility. Добавить в CI. Они закрывают пункты 4, 5, 8, 9 автоматически — не пропустят div с onClick без role, картинку без alt, форму без лейбла.
  2. До обеда — пройтись Tab'ом по всем основным сценариям. На каждый interactive element должен быть видимый фокус, переход должен идти в логичном порядке, ничего не должно «застревать».
  3. После обеда — открыть Chrome DevTools, на вкладке Issues включить Accessibility issues. Вкладка Lighthouse — прогнать audit. Не идеальный, но базовые проблемы покажет.
  4. К вечеру — добавить skip link, проверить landmarks, починить контрастность.

Это уровень 0. Дальше начинается уровень 1 — глубокая работа с aria, состояниями ошибок в формах, динамическими live-регионами для скринридеров. Но без уровня 0 туда лезть бессмысленно.

Что не делать

Самая частая ловушка — добавлять aria-label на всё подряд, надеясь, что это «улучшит доступность». Нет. ARIA должна дополнять там, где не хватает семантики, а не заменять её. <button aria-label="Закрыть">X</button> — нормально (текст внутри неинформативен). <a aria-label="Перейти на страницу профиля">Профиль</a> — лишний шум, ссылка и так озвучится «Профиль, ссылка».

Вторая ловушка — пытаться сразу пройти WCAG AAA. Это уровень для государственных сайтов и медицинских приложений. В обычном продукте AA достаточно, и до неё путь не такой долгий, если базовые вещи закрыты сразу.

Уровень 0 — это билет к разговору о доступности. Без него любые дискуссии про aria-live и keyboard traps бесполезны. С ним — у тебя есть фундамент, на котором можно строить дальше.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.