Как внедрить доступность в процесс
Как превратить доступность из разового аудита в привычку команды: пункт в 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, потом шлифовка.
- Один починенный компонент дизайн-системы чинит сотни страниц — это самое выгодное вложение в доступность.