Доступность в Generative UI: делаем AI-интерфейсы инклюзивными
Практическое руководство по обеспечению доступности генеративных интерфейсов для всех пользователей, включая работу со скринридерами и навигацию с клавиатуры.
Почему доступность сложнее в Generative UI
Ваша команда по доступности только что подписала ревью на каждый экран в продукте. Через три недели AI собирает макет, который ни один дизайнер не рисовал, — и в этом макете иерархия заголовков ломается для скринридеров, в сгенерированном диалоге появляется ловушка фокуса, а на графике цвет остаётся единственным сигналом. Ничего из этого не было поймано на ревью, потому что ничего из этого тогда не существовало.
Это новая поверхность доступности, и старый плейбук её не закрывает.
В традиционном UI разработчик может проверить каждый экран на соответствие требованиям WCAG 2.2. Экранов конечное количество. Команда по доступности (a11y) точно знает, что тестировать.
Generative UI разрушает эту модель. Множество возможных интерфейсов не поддаётся перечислению — AI может скомбинировать компоненты так, как ни один человек явно не проектировал. Экран, прошедший ревью по доступности сегодня, завтра может соединиться с новым компонентом и породить недоступный макет.
Решение — перенести требования к доступности на уровень отдельных компонентов. Если каждый компонент в библиотеке доступен сам по себе, любая их комбинация тоже будет доступна — при условии, что сама компоновка выстроена правильно. Эта оговорка несёт большую нагрузку; мы вернёмся к ней в разделе «Комбинаторные проблемы доступности», потому что именно там живёт большинство реальных багов a11y в генеративных системах.
Это на самом деле более чистая модель, чем ручной аудит каждого экрана. И это не факультатив: AI не добавит ARIA-атрибуты и не будет управлять фокусом за вас. Библиотека компонентов — ваш единственный рычаг влияния.
Базовые требования на уровне компонента
Каждый компонент в реестре инструментов Generative UI должен независимо соответствовать следующим требованиям:
Семантический HTML прежде всего. Используйте <button> для кнопок, <nav> для навигации, <table> для табличных данных. Не используйте <div onClick={...}> там, где подойдёт семантический элемент.
// Неправильно: div, притворяющийся кнопкой
<div className="button" onClick={handleClick}>Submit</div>
// Правильно: настоящий элемент button
<button type="button" onClick={handleClick}>Submit</button>
Все изображения имеют alt-текст. Для декоративных изображений: alt="". Для информационных — напишите описание.
Цвет — не единственный сигнал. График, где положительные значения отображаются зелёным, а отрицательные — красным, должен иметь дополнительный индикатор для пользователей, не различающих эти цвета: знак +/-, иконку или текстовую метку.
function TrendIndicator({ value }: { value: number }) {
const isPositive = value >= 0;
return (
<span
className={isPositive ? 'text-green-600' : 'text-red-600'}
aria-label={isPositive ? `Up ${Math.abs(value)}%` : `Down ${Math.abs(value)}%`}
>
{/* Icon provides visual signal beyond color */}
{isPositive ? '↑' : '↓'} {Math.abs(value)}%
</span>
);
}
Интерактивные элементы доступны с клавиатуры. Каждая кнопка, ссылка и элемент формы в компонентах должны получать фокус и полностью управляться с клавиатуры.
Достаточный размер целевых областей. WCAG 2.2, критерий 2.5.8 (Target Size, Minimum, уровень AA) требует минимум 24×24 CSS-пикселя; более раннее WCAG 2.1, критерий 2.5.5 (AAA), рекомендует 44×44. Для основных действий на мобильных устройствах ориентируйтесь на планку AAA — маленькие кнопки остаются одной из главных причин проблем с доступностью.
ARIA live regions для потокового контента
Стриминг — определяющая особенность Generative UI: компоненты появляются постепенно по мере того, как AI их генерирует. Скринридеры не объявляют динамически появляющийся контент автоматически. Им нужно явно об этом сообщить.
Используйте aria-live, чтобы объявлять о появлении нового сгенерированного контента:
// components/genui-output-region.tsx
export function GenUIOutputRegion({ children, isLoading }: {
children: React.ReactNode;
isLoading: boolean;
}) {
return (
<div
aria-live="polite"
aria-busy={isLoading}
aria-label="AI-generated content"
aria-atomic="false"
>
{children}
</div>
);
}
Ключевые решения:
aria-live="polite"объявляет новый контент при следующем удобном моменте — не перебивая пользователя, какassertive.aria-busy={isLoading}сообщает вспомогательным технологиям, что область обновляется. Скринридеры удерживают объявления до тех пор, покаaria-busyне станетfalse.aria-atomic="false"объявляет отдельные добавления по мере поступления, а не перечитывает всю область заново при каждом изменении.
Для состояния скелетона загрузки:
function LoadingSkeleton({ label }: { label: string }) {
return (
<div
role="status"
aria-label={`Loading ${label}`}
className="animate-pulse rounded-lg bg-muted h-32"
/>
);
}
role="status" — это неявная область aria-live="polite" для коротких статусных сообщений. Она объявляет о своём появлении, не прерывая текущую речь.
Управление фокусом
Когда сгенерированный контент появляется, фокус клавиатуры остаётся там, где был. Обычно это правильно — вы не хотите, чтобы фокус прыгал по экрану, пока AI стримит компоненты. Но в некоторых сценариях фокус нужно перемещать явно.
После отправки формы, заменяющей содержимое страницы:
const outputRef = useRef<HTMLDivElement>(null);
const [generatedUI, setGeneratedUI] = useState<React.ReactNode>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const ui = await generateUI(prompt);
setGeneratedUI(ui);
}
// Переводим фокус ПОСЛЕ того, как React закоммитил новый DOM, — никогда через setTimeout.
useEffect(() => {
if (generatedUI) {
outputRef.current?.focus();
}
}, [generatedUI]);
// tabIndex={-1} делает div фокусируемым программно
<div ref={outputRef} tabIndex={-1} aria-label="Generated results">
{generatedUI}
</div>
tabIndex={-1} делает элемент программно фокусируемым, не добавляя его в порядок табуляции. Пользователь может обойти его клавишей Tab естественным образом, но вы можете установить на него фокус через .focus().
Избегайте распространённого антипаттерна setTimeout(() => ref.current?.focus(), 50). 50 мс — это догадка; если рендер занимает больше времени на медленном устройстве, вызов .focus() уйдёт в устаревший или вовсе отсутствующий элемент. useEffect срабатывает после того, как React закоммитил новый DOM — именно та гарантия, которая вам нужна. Если действительно нужно отложить ещё на один тик (например, ждёте дочерний portal), используйте queueMicrotask, но никогда — таймаут с магическим числом.
После открытия диалога или панели с сгенерированным контентом:
Переведите фокус на первый фокусируемый элемент внутри панели или на её заголовок. При закрытии панели верните фокус на элемент, который её открыл.
Навигация с клавиатуры в генерируемых компонентах
Компоненты, появляющиеся в генерируемых макетах, должны быть полностью доступны с клавиатуры. Проверьте каждый компонент:
Таблицы: Пользователи скринридеров ожидают навигации по ячейкам с помощью клавиш-стрелок. Если ваш компонент DataTable не реализует это, сложные таблицы становятся барьером для навигации с клавиатуры.
Графики: Предоставляйте табличную альтернативу. SVG-графики визуально богаты, но для скринридеров практически бессмысленны. Добавьте элемент <details> или визуально скрытую таблицу с данными графика.
function BarChart({ title, data }: BarChartProps) {
return (
<div>
<h3>{title}</h3>
{/* Visual chart */}
<svg aria-hidden="true">
{/* ... chart rendering ... */}
</svg>
{/* Accessible data table, visually hidden */}
<details className="sr-only">
<summary>View data as table</summary>
<table>
<caption>{title}</caption>
<thead>
<tr><th>Category</th><th>Value</th></tr>
</thead>
<tbody>
{data.map(({ label, value }) => (
<tr key={label}>
<td>{label}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</details>
</div>
);
}
Класс sr-only скрывает таблицу визуально, сохраняя её в дереве доступности. aria-hidden="true" на SVG предотвращает попытки скринридера интерпретировать сырую SVG-разметку.
Уменьшенное движение
Часть пользователей настраивает операционную систему на предпочтение уменьшенного движения — потому что анимации вызывают физический дискомфорт у людей с вестибулярными расстройствами. Скелетоны загрузки и анимации переходов должны учитывать это предпочтение.
/* In your global CSS or Tailwind config */
@media (prefers-reduced-motion: reduce) {
.animate-pulse {
animation: none;
}
.transition-all {
transition: none;
}
}
В Tailwind можно использовать варианты motion-safe: и motion-reduce::
<div className="motion-safe:animate-pulse motion-reduce:opacity-50 bg-muted rounded-lg h-32" />
motion-safe: применяется только тогда, когда пользователь не запросил уменьшенное движение. motion-reduce: — когда запросил. Для состояний загрузки статичный, слегка затемнённый плейсхолдер — хорошая альтернатива пульсирующей анимации при включённом режиме уменьшенного движения.
Иерархия заголовков в составных макетах
AI компонует компоненты в макеты. У каждого компонента могут быть собственные заголовки. Когда несколько компонентов появляются вместе, их заголовки должны образовывать связную иерархию — не хаотичный набор разрозненных H2.
Это проблема компоновки, которую нельзя решить на уровне отдельного компонента. Каждый компонент должен принимать пропс с уровнем заголовка:
interface MetricCardProps {
label: string;
value: string;
change: number;
headingLevel?: 'h2' | 'h3' | 'h4'; // default to h3
}
function MetricCard({ label, value, change, headingLevel: Heading = 'h3' }: MetricCardProps) {
return (
<div className="rounded-lg border p-6">
<Heading className="text-sm font-medium text-muted-foreground">{label}</Heading>
{/* ... */}
</div>
);
}
В определении инструмента добавьте уровень заголовка как параметр, который AI может задавать:
metricCard: {
description: 'Display a KPI metric. Use headingLevel h2 for the first metric in a section, h3 for subsequent metrics.',
parameters: z.object({
label: z.string(),
value: z.string(),
change: z.number(),
headingLevel: z.enum(['h2', 'h3', 'h4']).default('h3'),
}),
}
Комбинаторные проблемы доступности
У модели «доступный компонент → доступная композиция» есть жёсткий предел: два компонента, по отдельности проходящие axe, при соседнем рендере могут вместе нарушать WCAG. Это баги, которые существуют только в генеративных системах, и они не всплывут ни в одном покомпонентном тесте.
Слом иерархии заголовков. Компонент A рендерит H2. Компонент B тоже рендерит H2. AI ставит их рядом в сетке карточек. В итоге скринридер сообщает о двух равноранговых разделах, которые должны были быть H3-детьми родительского H2. Митигация: параметризовать уровни заголовков (предыдущий раздел) и добавить интеграционный тест, который обходит дерево и проверяет монотонность уровней.
Конфликты ARIA-иерархии. Компонент Dialog ставит aria-modal="true". AI вкладывает в него ещё один Dialog (модели поручили отрисовать подтверждение внутри панели). На стеке два модальных окна — поведение вспомогательных технологий не определено. Митигация: ловить вложенные aria-modal на этапе рендера, отказываться рендерить внутренний диалог и логировать предупреждение в dev.
Дублирование лейблов. Два компонента SearchInput на одной сгенерированной странице выводят <label>Search</label>. У обоих инпутов одинаковое доступное имя — пользователь скринридера не может их различить. Митигация: делать пропс label обязательным (без значений по умолчанию) и в промпте для AI явно требовать называть каждый экземпляр.
Накопление live-областей. Три потоковых под-компонента оборачивают себя каждый в aria-live="polite". Скринридер выстраивает в очередь три накладывающихся объявления. Митигация: только самая внешняя область генеративного вывода объявляет aria-live; дочерние компоненты стримятся в неё как обычный DOM.
Эти баги не теоретические — это закономерный отказной режим систем «собери что угодно». Лечатся они на уровне интеграции: снять снапшоты репрезентативной выборки сгенерированных макетов, прогнать axe по комбинированным деревьям и добавить кастомные проверки под четыре паттерна выше.
Тестирование с реальными пользователями
Автоматические инструменты — axe-core, jest-axe, Storybook a11y, Lighthouse — ловят примерно 30% проблем доступности. (Это собственная оценка Deque Systems для axe-core, и она совпадает с тем, что скажет любая консалтинговая компания по доступности.) Остальные 70% — это вопросы суждения: понятен ли вообще объявляемый текст? Совпадает ли порядок фокуса с визуальным порядком, который ожидает зрячий пользователь? Может ли пользователь скринридера реально завершить задачу?
На эти вопросы CI-задача не ответит. Нужны живые люди.
Рабочий чеклист тестирования с реальными пользователями для релиза generative UI:
- Прогон со скринридером — NVDA на Windows + Firefox. Самая распространённая связка среди пользователей скринридеров в мире (опрос WebAIM). Прогоните топ-5 генеративных сценариев.
- Прогон со скринридером — VoiceOver на macOS + Safari и VoiceOver на iOS + Safari. Apple доминирует среди мобильных скринридеров.
- Прогон только с клавиатуры. Отключите мышь. Завершите каждую основную задачу с Tab, Shift+Tab, Enter, Space, Escape и клавишами-стрелками. Отметьте каждый исчезающий индикатор фокуса и каждую клавиатурную ловушку.
- Прогон голосового управления. Voice Control в macOS или Dragon. Generative UI печально известны как тяжёлые для голоса — лейблы генерируются AI, и это вскрывает дефекты именования, которые иначе не поймать.
- Реальные участники. Привлеките 2–4 пользователя скринридеров в квартал — через Fable, AccessWorks или через локальное a11y-сообщество. Одна такая сессия стоит больше, чем сотня автоматических прогонов.
- Высокий контраст и зум. Windows High Contrast + 200% зум браузера + 400% зум с reflow. Генеративные макеты часто ломаются на высоком зуме, потому что AI выдаёт фиксированные ширины.
- Уменьшенное движение. Включите системное предпочтение и заново прогоните стриминговые сценарии.
Заложите на это бюджет. Разумная частота для небольшой команды: автоматические проверки на каждом PR, четырёхчасовой ручной прогон перед каждым релизом и оплачиваемая внешняя сессия с участниками с инвалидностью раз в квартал.
ROI: как обосновать это инженерному руководству
Работа над доступностью конкурирует за инженерное время с фичами. Если вы инженерный менеджер, вам нужны цифры — и их нужно подать на языке, который понимает финансовый директор.
Стоимость. Встраивание доступности в библиотеку компонентов на этапе проектирования — примерно 5–10% от стоимости разработки компонента (оценки Forrester, команды a11y в Microsoft). Ретрофит недоступной библиотеки после запуска — 30–100%: вы перестраиваете компоненты и параллельно расплачиваетесь по долгу со всеми сломанными нижестоящими потребителями. Самый дешёвый доступный компонент — тот, который вы написали доступным с первого раза.
Риск. В рамках European Accessibility Act (EAA) правоприменение стартовало 28 июня 2025: B2C-цифровые услуги, продающиеся в ЕС, обязаны соответствовать EN 301 549 (что выровнено с WCAG 2.1 AA). Штрафы определяются на уровне страны-члена, но в ряде юрисдикций достигают шестизначных сумм в евро за нарушение. ADA в США генерирует порядка 4 000+ исков по веб-доступности в год (годовой отчёт UsableNet); средний размер мирового соглашения — 15–50 тыс. долл. США плюс обязательные доработки. UK Equality Act, канадский ACA, австралийский DDA добавляют сопоставимую экспозицию. Generative UI, массово выдающий несоответствующие макеты, — это, по сути, вероятностный генератор исков.
Выручка. Около 16% мирового населения живут со значимыми ограничениями здоровья (ВОЗ, 2023). Исследование «Click-Away Pound» в Великобритании оценило потери в 17,1 млрд фунтов в год — деньги, которые покупатели не оставляют в недоступных магазинах. Госконтракты в ЕС, США и Канаде требуют соответствия Section 508 / EN 301 549; недоступный продукт не может участвовать в тендере.
Сроки внедрения, в порядке приоритета. 90-дневный план для уже существующего generative UI:
| Неделя | Работа | Инженеро-дни |
|---|---|---|
| 1–2 | Аудит реестра компонентов axe + ручной прогон со скринридером; список дефектов по каждому компоненту | 5–8 |
| 3–4 | Чинить топ-10 компонентов (семантика HTML, фокус, лейблы) | 8–12 |
| 5–6 | Добавить общий aria-live-вывод, управление фокусом, поддержку reduced motion на уровне макета | 4–6 |
| 7–8 | Параметризовать уровни заголовков; добавить комбинаторные интеграционные тесты | 4–6 |
| 9–10 | Поднять jest-axe + Storybook a11y addon в CI; блокировать мердж при регрессиях | 2–3 |
| 11–12 | Первая внешняя сессия с пользователями скринридеров; чинить то, что они нашли | 3–5 |
| Дальше | Квартальное user testing, еженедельные автоматические drift-проверки | 1 день / неделю |
Итого: примерно 30–45 инженеро-дней, чтобы получить осмысленный базовый уровень на средней библиотеке компонентов, плюс поддержка дальше. Поднимайте это как инвестицию на один квартал, которая снимает целый повторяющийся класс юридических, выручковых и репутационных рисков.
Матрица приоритетов для триажа.
| Высокое влияние на пользователя | Низкое влияние на пользователя | |
|---|---|---|
| Высокий юридический риск | Чинить в этом квартале | Чинить в этом полугодии |
| Низкий юридический риск | Чинить в этом полугодии | В бэклог с датой |
Юридический риск высок, когда нарушение затрагивает транзакционный сценарий (чекаут, регистрация, управление аккаунтом) или любую государственную поверхность. Влияние на пользователя высокое, когда баг блокирует выполнение задачи для пользователей вспомогательных технологий, а не просто ухудшает комфорт.
Инструменты тестирования
Используйте эти инструменты для аудита библиотеки компонентов и генерируемых результатов. Версии ниже актуальны на середину 2025 — перед внедрением сверяйтесь с актуальными.
axe-core (axe-core@4.x, jest-axe@9.x): Автоматизированное тестирование доступности, обнаруживающее около 30% проблем. Интегрируйте с jest-axe для покрытия юнит-тестами.
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('MetricCard has no accessibility violations', async () => {
const { container } = render(
<MetricCard label="Revenue" value="$84,200" change={12.4} />
);
expect(await axe(container)).toHaveNoViolations();
});
Storybook Accessibility addon (@storybook/addon-a11y@8.x): Запускайте проверки axe прямо в Storybook в процессе разработки. Позволяет обнаружить проблемы до того, как они попадут в тесты.
Тестирование со скринридерами: NVDA (Windows, бесплатно) и VoiceOver (macOS, встроен) незаменимы для проверки того опыта, который автоматические инструменты не измеряют — насколько понятен генерируемый контент при прослушивании вслух? Расширенный чеклист — в разделе «Тестирование с реальными пользователями» выше.
Навигация только с клавиатуры: Отключите мышь и навигируйте по приложению исключительно с помощью Tab, Shift+Tab, Enter, Space и клавиш-стрелок. Это самый быстрый способ найти клавиатурные ловушки.
Обязательные требования: итоговый список
Перед выпуском функции Generative UI:
- Каждый компонент в реестре инструментов проходит проверку axe без нарушений
- Все интерактивные элементы доступны с клавиатуры и полностью ею управляемы
- Цвет никогда не является единственным носителем смысла
- Потоковый вывод обёрнут в область
aria-live(и только самая внешняя область её объявляет) - Скелетоны имеют
role="status"и информативныйaria-label - SVG-графики имеют табличную альтернативу с данными
- Все анимации учитывают
prefers-reduced-motion - Уровни заголовков параметризованы в компонентах, а не зашиты жёстко
- Комбинаторные интеграционные тесты покрывают как минимум четыре паттерна выше
- Минимум одна внешняя сессия user testing с пользователями скринридеров в квартал
Доступность, встроенная в библиотеку компонентов, — это не бремя. Именно она делает обещание «AI может скомпоновать что угодно» реальностью для всех пользователей. И именно она удерживает вас вне зала суда.
Связанные материалы: практическое руководство (Generative UI с React — практическое руководство) и гид по производительности (Оптимизация производительности Generative UI).
Создаёте доступный Generative UI для сложного приложения? Разберём вашу задачу вместе.
Alex
Generative UI Engineer & Consultant
Senior-инженер, специализирующийся на AI-интерфейсах и системах Generative UI. Помогаю продуктовым командам шипить быстрее с правильным GenUI-стеком.
Похожие статьи
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
Будьте в курсе Generative UI
Еженедельные статьи, обновления фреймворков и практические руководства — прямо в почту.
Нужна помощь с реализацией прочитанного?
Записаться на бесплатную консультацию