~/wiki / dostupnost / accessibility-v-ai-generatsii-interfeysov

AI генерирует недоступные интерфейсы: как это проверять и исправлять автоматически

Основной чат

Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.

$ cd раздел/ $ join vibe dev
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, если не сказать иначе.

Что должно быть в пайплайне, а не в голове дизайнера

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

Минимальный пайплайн проверки

  1. Линтер в IDE. eslint-plugin-jsx-a11y или аналог для вашего стека. Ругается на div onClick, отсутствующий alt, неправильные ARIA-атрибуты прямо в момент написания кода.
  2. Юнит-проверка компонентов. jest-axe или vitest-axe поверх тестов компонентов. Каждый новый компонент прогоняется через axe-core и падает, если нашлись нарушения.
  3. E2E-проверка ключевых экранов. Playwright + @axe-core/playwright на смоук-сценариях: главная, логин, чекаут, основной рабочий экран продукта. Проверка крутится в CI на каждый PR.
  4. Визуальный контраст. Storybook с аддоном a11y, либо отдельный шаг в CI, который проверяет контрасты по дизайн-токенам.
  5. Ручная проверка раз в спринт. 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 на живом продукте обычно даёт сотни нарушений. Это нормально и одновременно деморализующе. Если открыть отчёт и пойти чинить сверху вниз — застрянете на неделю в одном модуле.

Алгоритм разбора

  1. Сгруппируйте нарушения по правилу, а не по странице. Одно правило часто ломается в одном компоненте, который встроен в пятьдесят мест.
  2. Найдите источник. В девяти случаях из десяти это базовый компонент в дизайн-системе: Button, IconButton, Modal, Tabs, Tooltip.
  3. Чините в корне. Один фикс в IconButton закрывает сразу сотни нарушений button-name.
  4. Только после этого идите по «листовым» нарушениям — там, где проблема в конкретной странице, а не в системе.

Анти-паттерн: «починим страницу за страницей»

Команда берёт топ-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 за час

  1. Дизайнер в Figma помечает у каждой интерактивной ноды компонент из системы и подпись для скринридера в описании слоя
  2. Ассистент тянет фрейм, видит маппинг на компоненты, генерирует JSX строго на них
  3. Pre-commit прогоняет линт и axe по сторибуку затронутых компонентов
  4. CI на PR прогоняет axe по ключевым страницам и сравнивает с базлайном
  5. Ревьювер смотрит не на цвета, а на состояния и поведение клавиатуры

Если хоть один шаг выпал — экран приедет в прод с регрессом, и через месяц никто не вспомнит, кто его сгенерировал.

Как объяснить это команде и менеджменту

Доступность плохо продаётся словами «это правильно». Хорошо продаётся через риск, скорость и стоимость.

Аргументы, которые работают

  • Стоимость переделки. Починить компонент сейчас — час. Починить сто экранов через полгода — спринт.
  • Юридический риск. Для публичных продуктов и госсектора это не «было бы неплохо», это требование.
  • Скорость генерации. 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-генерации — это не отдельная дисциплина и не пункт в чеклисте перед релизом. Это свойство контракта между дизайн-системой, типами и инструментами. Сделайте контракт строгим — и ассистент будет работать на вас. Оставьте дыры — и он расширит их быстрее, чем любая ручная команда.

$ cd ../ ← назад к Доступность