Как внедрить доступность в процесс

Как превратить доступность из разового аудита в привычку команды: пункт в Definition of Done, линтер и axe в CI, разговор с бизнесом на его языке и план разбора уже существующего сайта.

Внедрение доступности — это перевод её из категории «проект» в категорию «процесс». Проект заканчивается, и сайт медленно откатывается назад. Процесс не даёт новым нарушениям попасть в главную ветку.

Почему разовый аудит не работает

Типичная история: компания заказала аудит, получила PDF на сорок страниц, команда две недели чинила, отчитались, выдохнули. Через полгода сайт снова недоступен — вышли три новые фичи, редизайн корзины и «быстрый» промо-баннер, и каждый принёс свои div-кнопки.

Дело не в лени. Дело в том, что доступность не была частью определения «готово». Задача считалась сделанной, когда её принял дизайнер и она не падала в проде. Никто ни разу не спросил: «а с клавиатуры она работает?» — и потому ответ никого не интересовал.

Внедрение сводится к четырём вещам: критерий готовности, автоматика в CI, поддержка бизнеса и план разбора старого кода. Разберём каждую.

1. Чек-лист в Definition of Done

Definition of Done (DoD) — согласованный командой список условий, без которых задача не считается завершённой. Именно сюда доступность нужно прописать: не в вики, не в «наши ценности», а в тот список, по которому задачу принимают.

Список должен быть коротким. Длинный чек-лист никто не читает — а с семи пунктами реально сверяются.

A11y-чек-лист (в DoD каждой UI-задачи)

[ ] Прошёл всю фичу с клавиатуры: Tab / Shift+Tab / Enter / Space / Esc
[ ] Фокус виден на каждом интерактивном элементе, порядок совпадает с визуальным
[ ] Интерактив — это button / a / input, а не div с onClick
[ ] У каждой иконочной кнопки и каждой картинки есть осмысленное имя или alt=""
[ ] Поля формы связаны с label; ошибка объявляется, а не только краснеет
[ ] Смысл не передаётся одним лишь цветом; контраст текста не ниже 4.5:1
[ ] axe DevTools на изменённом экране (включая открытые модалки) — 0 нарушений

Раз в спринт: пройти новый экран скринридером (NVDA или VoiceOver), 10 минут.

Два приёма, которые заметно повышают выживаемость такого списка. Первый — шаблон pull request: те же пункты чекбоксами в описании PR, чтобы автор ставил галочки сам, а ревьюер видел их перед глазами. Второй — шаблон задачи в трекере: у дизайн-макета сразу спрашивают состояния фокуса, текст ошибок и alt для картинок. Половина будущих багов исчезает ещё до строчки кода.

2. Автоматика: линтер и axe в CI

Ручной чек-лист держится на дисциплине, машина не устаёт. Автоматика не заменяет человека (мы помним про треть покрытия), но снимает с ревью весь пласт механических придирок.

Линтер: eslint-plugin-jsx-a11y

Этот плагин проверяет JSX прямо в редакторе, до запуска приложения. Ловит он ровно то, что видно в статике: onClick на div, картинку без alt, несуществующую ARIA-роль, autoFocus, tabindex больше нуля, поле без подписи.

npm install --save-dev eslint-plugin-jsx-a11y

Для классического конфига .eslintrc.json достаточно подключить рекомендованный набор и усилить пару правил до ошибок:

{
  "extends": ["plugin:jsx-a11y/recommended"],
  "plugins": ["jsx-a11y"],
  "rules": {
    "jsx-a11y/alt-text": "error",
    "jsx-a11y/anchor-is-valid": "error",
    "jsx-a11y/click-events-have-key-events": "error",
    "jsx-a11y/no-noninteractive-element-interactions": "error",
    "jsx-a11y/label-has-associated-control": "error",
    "jsx-a11y/no-autofocus": "warn"
  }
}

Важная деталь внедрения: если включить всё сразу как error на большом проекте, вы получите две тысячи ошибок и команду, которая на следующий день отключит плагин. Разумный порядок — включить набор в режиме warn, зафиксировать текущее число замечаний и запретить его рост, а новые файлы проверять строго. Можно даже начать с одного правила alt-text: оно бесспорное и чинится за минуты.

axe в тестах и в CI

Линтер видит только статический JSX. Всё, что появляется после рендера, — атрибуты, проставленные в рантайме, контраст, дублирующиеся id, состояния модалок — проверяет axe на живом DOM. В юнит-тестах это jest-axe, в e2e — @axe-core/playwright. Код ниже помечен как незапускаемый: ему нужны DOM и тестовый раннер.

// Компонентный тест: jest-axe + Testing Library
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { CheckoutForm } from './CheckoutForm';

expect.extend(toHaveNoViolations);

test('форма оформления заказа не нарушает WCAG A/AA', async () => {
  const { container } = render(<CheckoutForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
// E2E: @axe-core/playwright — проверяем реальные состояния страницы
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('корзина с открытой модалкой удаления доступна', async ({ page }) => {
  await page.goto('/cart');
  await page.getByRole('button', { name: 'Удалить «Кофемолка Wilfa»' }).click();
  await expect(page.getByRole('dialog')).toBeVisible();

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});

Обратите внимание на две вещи. Во-первых, тест ищет кнопку через getByRole по доступному имени — то есть сам факт того, что тест написался, уже доказывает наличие имени у кнопки. Во-вторых, скан запускается после открытия модалки: сканировать надо состояния, а не URL.

Дальше это вешается на pull request обычным шагом CI:

name: a11y
on: [pull_request]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx eslint . --max-warnings=0
      - run: npm run test:a11y        # jest-axe
      - run: npx playwright test a11y # @axe-core/playwright

Правило внедрения то же, что и с линтером: сначала пусть падение теста будет предупреждением, а через спринт-другой — блокирующим. Гейт, который команда научилась обходить в первую же неделю, хуже отсутствия гейта.

3. Как объяснить бизнесу

Фраза «это правильно, потому что нельзя дискриминировать пользователей» верна, но бюджет по ней не выделяют. Разговаривать нужно на языке рисков и денег — тем более что аргументы там честные.

АргументКак это звучит для бизнеса
Размер аудиторииПо оценке ВОЗ, около 16% людей в мире живут с той или иной формой инвалидности. Плюс временные и ситуативные ограничения: сломанная рука, яркое солнце на экране, одна рука занята ребёнком.
ВозрастАудитория 60+ — платёжеспособная и быстро растущая. Мелкий серый шрифт и зона клика 20 на 20 пикселей выкидывают её из воронки на первом же шаге.
SEOЗаголовки, alt-тексты, семантика и понятные ссылки — то же самое, что нужно поисковому роботу. Доступный сайт индексируется лучше.
Юридический рискВ США иски по ADA о недоступности сайтов идут тысячами в год; в ЕС с 2025 года действует European Accessibility Act. Для компаний с зарубежными клиентами это не абстракция.
Госзаказ и тендерыТребование доступности всё чаще прямо записано в условиях. Нет доступности — нет заявки.
Стоимость правкиДоступность, заложенная в компонент сразу, стоит около нуля. Она же, вкрученная в дизайн-систему через два года, — это отдельный квартальный проект.
Качество продуктаТот же набор правок чинит мобильную версию, ускоряет клавиатурные сценарии для «продвинутых» пользователей и делает возможным e2e-тестирование по ролям.

И один приём сильнее всех таблиц: покажите. Включите VoiceOver на телефоне и попробуйте вместе с владельцем продукта оформить заказ на собственном сайте. Пять минут такой демонстрации делают для бюджета больше, чем любая презентация.

4. Аудит существующего сайта: с чего начинать

Если сайт большой и живой, попытка «починить всё» гарантированно захлебнётся. Работает приоритизация по двум осям: важность сценария и тяжесть барьера.

Ось первая — где чиним. Возьмите аналитику и выпишите ключевые пути: главная → каталог → карточка товара → корзина → оплата. Плюс регистрация, вход и восстановление пароля. Это 5–10 экранов, через которые проходят все. Страница «Наши ценности» подождёт.

Ось вторая — что чиним в первую очередь. Барьеры сильно различаются по цене для пользователя.

ПриоритетБарьерЧто происходит с пользователем
P0 — блокерловушка фокуса, недостижимая с клавиатуры кнопка, форма без подписей, капча без альтернативысценарий невозможно завершить вообще
P1 — тяжёлыйошибки формы не объявляются, безымянные иконочные кнопки, контраст ниже нормы, пропущенные заголовкизавершить можно, но методом угадывания
P2 — трениенет skip-link, неудобный порядок фокуса, шумный alt, лишние ARIA-атрибутыдольше и раздражительнее

Дальше — план на квартал, а не на вечность. Волна первая: прогнать axe по десяти ключевым экранам и починить все P0 (обычно это неделя-две работы). Волна вторая: сами компоненты — привести в порядок кнопки, поля, модалку и таблицу в дизайн-системе; исправление одной кнопки в общей библиотеке чинит её во всех двухстах местах разом. Волна третья: включить линтер и CI, чтобы новые нарушения больше не попадали в мастер. Волна четвёртая: пройти ключевые сценарии скринридером и заняться P1 и P2.

Порядок здесь неслучаен: сначала останавливаем кровотечение (P0), потом чиним источник (компоненты), потом ставим замок на дверь (CI) — и только после этого шлифуем. Если поменять шаги местами и начать со шлифовки, вы будете бесконечно чинить то, что параллельно ломается в новых фичах.

Как это работает

За всей этой конструкцией стоит одна простая мысль: доступность держится не на знаниях отдельных людей, а на устройстве процесса. Знающий фронтендер уйдёт в другую команду, а линтер, тест и пункт в DoD останутся.

Каждый слой ловит свой класс дефектов и стоит по-разному. Линтер срабатывает за секунды в редакторе и стоит почти ничего. Тест в CI ловит рантайм-нарушения и стоит минуты машинного времени. Ревью по чек-листу ловит бессмысленные имена и стоит внимания человека. Проход скринридером ловит сломанные сценарии и стоит дороже всего — поэтому его и делают точечно, по новым экранам, а не сплошняком каждый спринт.

Плюс отдельно стоит дизайн-система: она множит любое решение на количество мест использования. Один правильно сделанный компонент Button закрывает сотни потенциальных нарушений, поэтому вкладываться в неё выгоднее, чем в починку отдельных страниц.

Частые ошибки

  • Назначить «ответственного за доступность». Один энтузиаст на команду из десяти человек становится узким горлышком и выгорает. Доступность — часть определения «готово» для всех, как и тесты.
  • Включить все правила линтера строго и сразу. Две тысячи ошибок в первый день = плагин отключат во второй. Заходите через warn и запрет на рост числа замечаний.
  • Поставить оверлей-виджет вместо работы. Скрипты вида «кнопка доступности» — тот самый костыль, который обещает WCAG за одну строчку. Сообщество пользователей скринридеров относится к ним резко отрицательно, а от исков в США они не спасли ни разу: нарушения остаются в разметке.
  • Чинить всё подряд «по отчёту». Отчёт axe по всему сайту даст тысячу строк, из которых 800 — про одну и ту же кнопку из общей библиотеки. Группируйте по компонентам, а не по страницам.
  • Считать доступность фичей. «Сделаем доступность в третьем квартале» звучит так же, как «добавим безопасность в третьем квартале». Это не пункт бэклога, а свойство того, что вы уже пишете.
  • Молча упавший CI. Если a11y-тест падает и все привыкли перезапускать сборку, гейта уже нет. Либо тест блокирует мерж, либо его нет вовсе.

Итоги

  • Разовый аудит откатывается назад. Работает только процесс: критерий готовности, автоматика, поддержка бизнеса, план по легаси.
  • Семь пунктов a11y в Definition of Done и в шаблоне pull request дают больше, чем стостраничный гайд в вики.
  • eslint-plugin-jsx-a11y ловит статические нарушения в JSX; jest-axe и @axe-core/playwright — рантайм, включая открытые модалки.
  • Вводите гейты постепенно: warn → запрет роста → блокирующая ошибка. Резкое включение убивает инициативу.
  • Бизнесу говорите про аудиторию, возраст, SEO, ADA и European Accessibility Act, тендеры и стоимость поздней правки — а лучше просто покажите VoiceOver на своём сайте.
  • Аудит легаси приоритизируйте по двум осям: ключевые сценарии и тяжесть барьера. Сначала P0 на десяти главных экранах, потом компоненты, потом CI, потом шлифовка.
  • Один починенный компонент дизайн-системы чинит сотни страниц — это самое выгодное вложение в доступность.
Проверьте себя
1. Команда подключает eslint-plugin-jsx-a11y к большому легаси-проекту. Как это лучше сделать?
AВключить все правила как error сразу — иначе никто не будет их чинить
BВключить набор в режиме warn, зафиксировать текущее число замечаний и запретить его рост, а потом постепенно ужесточать
CНе подключать линтер вообще: он всё равно ловит только статические нарушения
DПодключить, но добавить eslint-disable в файлы, где много ошибок
2. Сайт большой, доступностью не занимались. С чего начать аудит?
AПрогнать axe по всем страницам и чинить отчёт сверху вниз
BПоставить оверлей-виджет «кнопка доступности» — это быстрее всего
CВзять ключевые сценарии (каталог, корзина, оплата, вход) и устранить на них барьеры уровня P0, которые вообще не дают завершить сценарий
DНачать с alt-текстов и контраста на всех страницах сразу