Доступность нельзя выкатить как факт веры - её нужно измерять, а измерение делится на два слоя, которые часто путают. Первый слой автоматический: программы, читающие разметку и правила. Второй ручной: человек, который проходит интерфейс так, как это сделает реальный пользователь. Соблазн - остановиться на первом: он быстрый, дешёвый и даёт зелёную галочку в CI. Но зелёная галочка отвечает не на тот вопрос.
Автоматические инструменты отлично находят то, что машинно проверяемо: отсутствие alt у картинки, поле формы без связанной подписи, недостаточный контраст текста, дубли id, кнопку без доступного имени. Набор устоявшийся. eslint-plugin-jsx-a11y ловит нарушения прямо в редакторе, пока код пишется. axe-core - движок правил, на котором держится большинство проверок. Lighthouse даёт сводную оценку страницы. Playwright с axe гоняет те же правила в браузере на живом рендере. HTML-валидатор ловит невалидную вложенность, которая ломает дерево доступности. Это дешёвая и обязательная гигиена.
На этом потолок автоматики и заканчивается. Ни один движок не скажет вам, понятен ли alt по смыслу: alt=img_4832 пройдёт проверку на наличие атрибута, хотя незрячему покупателю он не сообщает ничего. Машина не оценит, логичен ли порядок фокуса, ведёт ли ссылка туда, куда обещает текст, и выполнима ли вообще задача - оформить заказ от корзины до подтверждения. Оценки расходятся, но сходятся в порядке величины: автоматика покрывает примерно от трети до половины критериев WCAG. Остальное - смысл, контекст и поведение - видит только человек.
Поэтому автоматику встраивают в две точки конвейера. Линтер работает на этапе письма и не даёт нарушению попасть в коммит. axe в тестах Playwright работает в CI и стережёт критичные страницы от регрессий: если на странице оформления заказа появилось серьёзное нарушение, сборка падает, и никто не выкатит поломку в прод. Такой тест дёшев в поддержке и окупается первым же пойманным регрессом.
Ручная проверка начинается с клавиатуры, потому что она обнажает структуру. Уберите мышь и пройдите сценарий одним Tab: каждый интерактивный элемент обязан получать фокус, видимое кольцо фокуса - присутствовать, порядок обхода - совпадать с визуальным, а из любого компонента должен быть выход, без ловушек. Если до кнопки Купить нельзя добраться с клавиатуры, значит, часть покупателей не купит ничего, и ни один axe этого не сообщит.
Клавиатурой список не исчерпывается. Масштаб 200-400% проверяет, не рассыпается ли вёрстка и не теряется ли контент при увеличении. Настоящий скринридер - VoiceOver на macOS, NVDA на Windows - показывает, как страница звучит, а не как выглядит. Режим reduced motion проверяет, уважает ли интерфейс просьбу убрать анимацию. Forced colors - переживает ли он принудительную палитру высокого контраста. Полезно держать эти слои перед глазами разом.
| Слой | Проверка | Что ловит |
|---|---|---|
| Автоматика | линтер + axe | синтаксис: alt, подписи, контраст, id |
| Клавиатура | только Tab | фокус, порядок обхода, ловушки |
| Масштаб | 200-400% | переполнение и потерю контента |
| Скринридер | VoiceOver, NVDA | звучание, имена, порядок чтения |
| Движение | reduced motion | уважение к настройке пользователя |
| Цвета | forced colors | выживание в контрастной палитре |
Показательный провал выглядит так: страница набирает 100 из 100 в Lighthouse, команда празднует, а через неделю приходит жалоба - незрячий пользователь не может завершить оплату, потому что после открытия модального окна фокус остаётся под ним, на скрытой странице. Автоматика была права в своих границах и слепа за ними. Дисциплина проста: автоматику - в каждый коммит и в CI как обязательный минимум, ручную проверку ключевых сценариев - перед релизом, и никогда не путать зелёную галочку с работающим для человека интерфейсом.
npm i -D @axe-core/playwright
import AxeBuilder from '@axe-core/playwright';
test('page has no serious a11y violations', async ({ page }) => {
await page.goto('/checkout');
const result = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(result.violations).toEqual([]);
});