AI генерирует недоступные интерфейсы: как это проверять и исправлять автоматически
Основной чат
Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.
Откройте инспектор в браузере и пройдитесь Tab по последнему экрану, который сгенерировал вам AI-ассистент. Скорее всего, фокус прыгает не туда, половина кнопок — это div с onClick, а модалка закрывается только мышкой. Визуально всё красиво. Функционально — стена для любого, кто не пользуется мышью и зрячим взглядом.
AI пишет интерфейсы быстро. Доступности он по умолчанию не делает — он копирует то, чему его научили, а в обучающих данных полно div-ов вместо кнопок, картинок без alt и контрастов на грани читаемости. Получается парадокс: чем больше команда генерирует кода через ассистентов, тем хуже становится средний уровень accessibility в проекте. И это видно не на ревью у дизайнера, а у пользователя со скринридером.
Хорошая новость: большую часть этих проблем можно ловить автоматически. Плохая — никто не настраивает пайплайн, пока не прилетит жалоба или иск.
Почему AI-сгенерированные интерфейсы ломают доступность
Дело не в злом умысле модели. Дело в том, как она устроена.
Модель оптимизирует визуальный результат. Если промпт звучит как «сделай красивую карточку товара», модель сделает красиво. Семантика, фокус, контраст — это не часть награды.
Обучающие данные грязные. Большая часть открытого фронтенд-кода написана без оглядки на a11y. Модель учится на медиане, а медиана плохая.
Промпты редко содержат требования доступности. Дизайнер пишет «герой-секция с CTA», а не «герой-секция с CTA, контраст AA, фокус-стейты, семантический button, aria-label если иконка одна».
Скриншот в Figma MCP не несёт семантики. Когда вы прокидываете макет в ассистента через MCP-сервер, он видит пиксели и слои, а не роль элемента. «Это кнопка или ссылка» — он решает на глаз.
Типичные паттерны, которые я ловлю на ревью
divиspanс обработчиком клика вместоbuttonилиa- Иконочные кнопки без
aria-label— скринридер читает «кнопка», и всё - Картинки и иллюстрации с пустым или мусорным
alt(«image», «picture1») - Цветной текст на цветной плашке с контрастом ниже 3:1
- Формы без
label, с плейсхолдером в роли подписи - Модалки без trap focus и без возврата фокуса на триггер
- Состояния ошибок, переданные только цветом, без иконки и текста
- Кастомные селекты и табы без ARIA-ролей и клавиатурной навигации
- Анимации без учёта
prefers-reduced-motion
Это не экзотика. Это то, что вылетает из ассистента в 8 случаях из 10, если не сказать иначе.
Что должно быть в пайплайне, а не в голове дизайнера
Главная ошибка — думать, что доступность ловится глазами на ревью. Глаза устают, ревью пропускают, новые экраны генерируются быстрее, чем команда успевает их смотреть. Нужны автоматические шлюзы.
Минимальный пайплайн проверки
- Линтер в IDE.
eslint-plugin-jsx-a11yили аналог для вашего стека. Ругается наdiv onClick, отсутствующийalt, неправильные ARIA-атрибуты прямо в момент написания кода. - Юнит-проверка компонентов.
jest-axeилиvitest-axeповерх тестов компонентов. Каждый новый компонент прогоняется через axe-core и падает, если нашлись нарушения. - E2E-проверка ключевых экранов. Playwright +
@axe-core/playwrightна смоук-сценариях: главная, логин, чекаут, основной рабочий экран продукта. Проверка крутится в CI на каждый PR. - Визуальный контраст. Storybook с аддоном a11y, либо отдельный шаг в CI, который проверяет контрасты по дизайн-токенам.
- Ручная проверка раз в спринт. Tab по флоу, VoiceOver или NVDA на ключевом сценарии. Автомат ловит 40-60% проблем, остальное — только живой обход.
Если в пайплайне нет хотя бы первых трёх пунктов, AI будет стабильно проносить мимо вас недоступный код, и вы об этом узнаете от пользователя.
Антипаттерн: «доступность добавим потом»
В команде это звучит как «давайте сначала запустим, потом починим». На практике «потом» означает рефакторинг половины компонентной библиотеки, потому что div-кнопки расползлись по всему продукту и поменять их разом нельзя — слишком много мест ломается. Дешевле поставить шлюз на входе.
Как формулировать промпты, чтобы AI не генерил мусор
Промпт — это ТЗ. Если в ТЗ нет требований доступности, их не будет и в результате.
Шаблон промпта для генерации компонента
- Назначение компонента и роль (кнопка действия, ссылка, переключатель)
- Семантический тег, который ожидается на выходе
- Поведение с клавиатуры: какие клавиши, какой порядок фокуса
- ARIA-атрибуты, если компонент не нативный
- Состояния: hover, focus-visible, active, disabled, loading, error
- Контраст: минимум AA для текста, AA Large или 3:1 для интерактивных границ
- Поведение при
prefers-reduced-motion
Звучит занудно. Но один раз вынесенный в системный промпт или сниппет, этот блок экономит часы ревью.
Вопросы, которые задаю себе перед мержем AI-сгенерированного экрана
- Могу ли я пройти весь сценарий с клавиатуры, не трогая мышь?
- Виден ли фокус на каждом интерактивном элементе?
- Если выключить цвет — понятно ли, где ошибка, где успех, где активное состояние?
- Что прочитает скринридер на иконочной кнопке?
- Что произойдёт с модалкой, если нажать Esc и Tab внутри неё?
- Анимация уважает системную настройку «уменьшить движение»?
Если на любой вопрос ответ «не знаю» — экран не готов, независимо от того, насколько красиво он выглядит в Figma.
Короткий итог сегмента. AI не ломает доступность специально — он просто оптимизирует не то. Пока в пайплайне нет автоматических шлюзов и в промптах нет требований к семантике, фокусу и контрасту, каждый новый сгенерированный экран добавляет долг. Дальше разберём, какие именно проверки стоит ставить в CI, как читать их отчёты и что делать с найденными нарушениями, не превращая работу в бесконечный багфикс.
CI: какие именно проверки и где их ставить
Когда говорю «поставьте a11y-проверки в CI», слышу в ответ «у нас уже есть линтер». Линтер — это первая линия, она ловит синтаксис. CI должен ловить поведение и итоговую разметку.
Слои проверок в порядке стоимости
- Pre-commit (секунды). Линтер на изменённые файлы. Не пускает в коммит
divсonClickбез роли,imgбезalt, неизвестные ARIA-атрибуты. - PR-чек на компонентах (минуты). Юнит-тесты с axe прогоняются по сторибуку или по тестовым обвязкам компонентов. Падают на конкретном компоненте, а не «где-то в проекте».
- PR-чек на страницах (минуты-десятки минут). Playwright поднимает приложение, ходит по ключевым роутам, прогоняет axe и проверяет focus-trap, порядок Tab, наличие skip-link.
- Ночной прогон (часы). Полный обход продукта с разными ролями пользователей, разными темами, локалями и масштабом 200%. Сюда же — визуальная регрессия фокус-стилей.
Важно не складывать всё в одну стадию. Если a11y-тесты висят 20 минут на каждый PR, их начнут отключать. Быстрые проверки — на каждый коммит, тяжёлые — ночью.
Что должно блокировать мерж, а что — только предупреждать
| Уровень | Реакция CI |
|---|---|
| Critical (нет alt, нет label у инпута, контраст ниже 3:1 у текста, ловушка фокуса) | Блок мержа |
| Serious (неверный ARIA, нет focus-visible, ошибки порядка заголовков) | Блок мержа |
| Moderate (декоративные элементы в табордере, избыточный ARIA) | Предупреждение |
| Minor (стилистические замечания) | Предупреждение |
Без этого деления команда либо игнорирует жёлтый отчёт, либо ненавидит красный.
Как читать отчёт и не утонуть
Первый прогон axe на живом продукте обычно даёт сотни нарушений. Это нормально и одновременно деморализующе. Если открыть отчёт и пойти чинить сверху вниз — застрянете на неделю в одном модуле.
Алгоритм разбора
- Сгруппируйте нарушения по правилу, а не по странице. Одно правило часто ломается в одном компоненте, который встроен в пятьдесят мест.
- Найдите источник. В девяти случаях из десяти это базовый компонент в дизайн-системе:
Button,IconButton,Modal,Tabs,Tooltip. - Чините в корне. Один фикс в
IconButtonзакрывает сразу сотни нарушенийbutton-name. - Только после этого идите по «листовым» нарушениям — там, где проблема в конкретной странице, а не в системе.
Анти-паттерн: «починим страницу за страницей»
Команда берёт топ-10 страниц по трафику и правит руками. Через месяц AI-ассистент сгенерирует одиннадцатую и принесёт ровно те же ошибки. Чинить надо компоненты и промпты, не страницы.
Что делать с тем, что автомат не ловит
Axe и его собратья честно говорят: они находят 30-50% реальных проблем. Остальное — про смысл, а не про разметку.
Сценарии, которые видит только живой обход
- Логика фокуса после действия. Удалили строку в таблице — фокус улетел в
body. Технически чисто, по факту — скринридер теряется. - Порядок чтения, не совпадающий с визуальным. Карточка визуально читается слева направо, в DOM — наоборот. axe доволен, пользователь скринридера — нет.
- Иконочные кнопки с «понятным» лейблом.
aria-label="Действие"проходит проверку, но не отвечает на вопрос «какое действие». - Динамический контент без объявления. Тост появился, исчез — скринридер не сказал ничего, потому что нет
aria-live. - Состояния, переданные только движением. Подсказка появляется на hover и исчезает через секунду. Клавиатура и тач — мимо.
Чеклист дизайнера прямо в Figma
- У каждого интерактивного элемента в макете есть состояние focus, не только hover
- Иконочные кнопки имеют подпись для скринридера — пишу её рядом в комментарии к фрейму
- В прототипе есть порядок Tab, согласованный с визуальным
- В спеке модалки указано: куда улетает фокус при открытии, куда возвращается при закрытии, что делает Esc
- Контраст проверен не только для основного фона, но и для hover/active/disabled
- Для анимаций отмечено, что происходит при
prefers-reduced-motion: reduce
Это не «дополнительная работа». Это то, что вы всё равно потом обсудите с разработчиком — лучше заранее, в макете.
Вопросы для парного ревью дизайн + фронт
- Где живёт этот компонент в системе и какие у него ARIA-контракты?
- Что произойдёт, если пользователь придёт сюда из скринридера, не видя экрана?
- Какие состояния мы забыли нарисовать — loading, empty, error, no-permission?
- Если AI сгенерирует похожий компонент завтра, на что упадёт CI?
Последний вопрос — самый полезный. Он переводит разговор с «нашли баг» на «закрыли класс багов».
Короткий итог сегмента. Доступность не чинится героическим спринтом по страницам. Она держится на трёх вещах: проверки разной стоимости в разных стадиях CI, разбор отчётов через компоненты дизайн-системы, и дизайнерская спецификация, в которой состояния, фокус и поведение скринридера описаны до того, как ассистент сгенерирует первую строчку кода. Дальше разберём, как встроить это в дизайн-систему и в работу с AI-инструментами вроде Figma MCP, чтобы новые экраны рождались уже доступными.
Доступность как контракт дизайн-системы
Если AI генерирует интерфейс, который ломает доступность, виноват не AI. Виноват контракт компонента, который этого не запрещает. Дизайн-система — это место, где правила должны быть жёсткими настолько, чтобы их нельзя было нарушить случайно.
Что значит «доступный по контракту»
Компонент IconButton, у которого пропс label обязательный и без него падает TypeScript — это контракт. Компонент, который сам прокидывает aria-label, aria-expanded, aria-controls из понятных пропсов — это контракт. Сторибук, в котором каждый компонент имеет вкладку «a11y» с прогоном axe — это контракт.
AI-ассистент, обученный на вашем коде, начнёт повторять то, что видит. Если в кодовой базе сто IconButton без подписи — он сгенерирует сто первый. Если их ноль, потому что иначе сборка падает — он сгенерирует с подписью.
Минимальный набор того, что должно жить в системе
- Компоненты с обязательными пропсами доступности на уровне типов
- Сторибук с axe-аддоном и фейл-режимом на критических правилах
- Линт-правила: запрет
divсonClick, запрет позитивныхtabindex, запрет картинок безalt - Snapshot-тесты ARIA-атрибутов для ключевых интерактивных компонентов
- Документация состояний: focus, hover, active, disabled, loading, error — в одном месте, рядом с компонентом
Работа с AI-ассистентами и Figma MCP
Когда дизайнер тянет макет через MCP в Cursor или Claude Code, и ассистент превращает фрейм в JSX, в этот момент происходит самая дорогая ошибка: контекст обрывается. AI видит вёрстку, но не видит спецификацию фокуса, поведения скринридера и состояний.
Что подкладывать в контекст ассистенту
- Файл с правилами доступности проекта — короткий, на одну страницу, без воды
- Ссылки на базовые компоненты дизайн-системы, а не на сторонние библиотеки
- Описание поведения: «модалка открывается → фокус на первом интерактивном элементе → Esc закрывает → фокус возвращается на триггер»
- Запрет на изобретение собственной семантики там, где есть готовый компонент
Анти-паттерн: «AI сам разберётся»
Промпт «сделай красиво и доступно» не работает. Ассистент сгенерирует то, что чаще встречается в его обучении: div с role="button", иконку без подписи, модалку без ловушки фокуса. Это не злой умысел — это медиана интернета.
Работает противоположное: давать ассистенту узкий каркас. «Используй Button из @/ui, label обязателен, иконка через пропс icon». В таком режиме у AI меньше свободы — и меньше способов сломать доступность.
Сценарий: новый экран через MCP за час
- Дизайнер в Figma помечает у каждой интерактивной ноды компонент из системы и подпись для скринридера в описании слоя
- Ассистент тянет фрейм, видит маппинг на компоненты, генерирует JSX строго на них
- Pre-commit прогоняет линт и axe по сторибуку затронутых компонентов
- CI на PR прогоняет axe по ключевым страницам и сравнивает с базлайном
- Ревьювер смотрит не на цвета, а на состояния и поведение клавиатуры
Если хоть один шаг выпал — экран приедет в прод с регрессом, и через месяц никто не вспомнит, кто его сгенерировал.
Как объяснить это команде и менеджменту
Доступность плохо продаётся словами «это правильно». Хорошо продаётся через риск, скорость и стоимость.
Аргументы, которые работают
- Стоимость переделки. Починить компонент сейчас — час. Починить сто экранов через полгода — спринт.
- Юридический риск. Для публичных продуктов и госсектора это не «было бы неплохо», это требование.
- Скорость генерации. AI-инструменты ускоряют выпуск экранов в разы. Без контракта они так же ускоряют выпуск багов.
- Качество найма. Сильные инженеры и дизайнеры читают вашу систему. Если в ней
div onClick— это видно сразу.
Вопросы для ревью с продактом
- Какой процент пользователей мы теряем на клавиатурной навигации и не знаем об этом?
- Что произойдёт с метриками, если флоу станет проходимым для скринридера?
- Какой компонент чаще всего генерирует ассистент и насколько он соответствует контракту?
- Что мы добавляем в CI, чтобы следующий регресс не уехал в прод?
Короткий итог сегмента. AI не делает интерфейсы недоступными — он отражает то, что уже лежит в кодовой базе и в дизайн-системе. Поэтому работа идёт не с моделью, а с контрактом: типы, линты, сторибук, MCP-каркас, описание состояний в макете. Дальше посмотрим, как собрать всё это в один рабочий процесс, который выдерживает скорость AI-генерации и не превращает доступность в отдельный проект «когда-нибудь потом».
Финальный чеклист: что должно быть в проекте
Этот список — не идеал, а минимум, ниже которого AI-генерация начинает протекать в прод. Если хоть одна строка не закрыта, регресс — вопрос недель.
Дизайн-система
- У каждого интерактивного компонента описаны состояния focus, hover, active, disabled, loading, error
- Кнопка, ссылка, чекбокс, радио, селект, инпут — компоненты системы, а не локальные обёртки
- Иконка-кнопка требует
aria-labelчерез тип пропсов, не опционально - Модалка, попап, тултип, тост — один компонент на проект, не пять
Код и инструменты
- ESLint с
jsx-a11yв режиме error на критических правилах - Сторибук с axe-аддоном, билд падает на violations уровня serious и critical
- Snapshot или интеграционные тесты на ARIA-атрибуты ключевых компонентов
- Pre-commit хук гоняет линт по изменённым файлам
- CI прогоняет axe по списку ключевых страниц и держит базлайн
AI и MCP
- В контексте ассистента лежит короткий файл правил доступности
- В макете каждая интерактивная нода размечена компонентом дизайн-системы
- Промпты содержат явный запрет на
divсonClickи собственную семантику - Сгенерированный код проходит те же линты и тесты, что и ручной
Анти-паттерны, которые встречаются чаще всего
«Сделаем доступность отдельным спринтом»
Никогда не наступает. Через квартал кодовая база разрастается, и спринт превращается в полгода. Доступность чинится только инкрементально, в момент работы с компонентом.
«У нас же есть дизайн-система»
Система есть, а контракта на её использование нет. Полкоманды собирает экраны из компонентов, полкоманды — из голых тегов, потому что «так быстрее». AI повторяет это распределение в той же пропорции.
«Скринридером никто не пользуется»
В метриках это не видно, потому что такие пользователи уходят молча после первого экрана. В саппорт они тоже не пишут — писать неудобно. Отсутствие жалоб не равно отсутствию проблемы.
«AI сгенерил — потом поправим»
«Потом» означает следующий рефакторинг, который не случится. Сгенерированный код уходит в прод, копируется в соседние экраны, становится новым образцом для AI в следующей задаче. Цикл замыкается.
«Добавим aria-label везде, где сомневаемся»
Лишние ARIA-атрибуты ломают скринридер сильнее, чем их отсутствие. role="button" на нативной кнопке, дублирующий aria-label поверх видимого текста, aria-hidden на интерактивном элементе — частые подарки от ассистента. Лучше нативная семантика без ARIA, чем ARIA поверх неё.
Вопросы для ревью PR с AI-сгенерированным кодом
Эти вопросы стоит держать в голове, читая diff. Половина из них закрывается за минуту, если ответ «нет».
- Все интерактивные элементы — это нативные теги или компоненты системы?
- Можно ли пройти флоу только клавиатурой, не теряя фокус?
- Каждое поле формы связано с лейблом через
for/idили обёртку? - Иконки без текста имеют доступное имя? Декоративные —
aria-hidden? - Состояние ошибки доступно скринридеру, а не только подсвечено красным?
- Модалка ловит фокус, закрывается по Esc и возвращает фокус на триггер?
- Динамические изменения объявляются через
aria-live, где это уместно? - Контраст текста и интерактивных границ проходит по дизайн-токенам, а не «на глаз»?
Что делать в понедельник утром
Не пытайтесь починить всё сразу. Возьмите один компонент, который чаще всего генерирует ассистент — обычно это кнопка или поле ввода. Опишите его контракт, закройте линтом, добавьте axe в сторибук. Прогоните по нему уже сгенерированные экраны и почините регрессы.
Дальше — следующий компонент. Через месяц система начнёт сама сопротивляться плохому коду, и AI, который читает её типы и примеры, начнёт генерировать ровно то, что вы зашили в контракт.
Короткий итог. Доступность в эпоху AI-генерации — это не отдельная дисциплина и не пункт в чеклисте перед релизом. Это свойство контракта между дизайн-системой, типами и инструментами. Сделайте контракт строгим — и ассистент будет работать на вас. Оставьте дыры — и он расширит их быстрее, чем любая ручная команда.