ARIA атрибуты в React: как проектировать доступные компоненты без лишней магии
ARIA атрибуты в React на практике: когда они нужны, как делать формы, модалки и табы, где команды ошибаются в проде и что говорить на интервью.
- Введение
- ARIA в React: где нужна, а где только мешает
- Правило no ARIA is better than bad ARIA
- Какие группы атрибутов используются чаще всего
- Архитектура доступности: слой семантики поверх дизайн-системы
- Схема компонентов
- Поток управления
- Границы ответственности
- Узкие места и отказоустойчивость
- Практика: рабочие ARIA-паттерны в React
- Пример 1: иконка-кнопка с корректным названием
- Пример 2: поле формы с подсказкой и динамической ошибкой
- Пример 3: модалка через portal с role="dialog"
- Таблица выбора: нативный HTML, headless-компоненты или кастом
- Типичные проблемы в реальных проектах: что ломается после релиза
- 1. Неполная синхронизация состояния
- 2. Потеря фокуса при условном рендеринге
- 3. "Шумные" обновления динамической области
- 4. Переопределение роли без бизнес-причины
- 5. Доступность проверяют только на уровне линтера
- Разбор производительности: где реальная цена ARIA
- Практики команды: как поддерживать доступность без авралов
- Частые ошибки
- Как отвечать на интервью про ARIA в React
- FAQ
- Нужно ли добавлять ARIA почти в каждый React-компонент?
- Что выбрать: aria-label или aria-labelledby?
- Можно ли сделать доступный интерфейс только на UI-библиотеке?
- Почему после внедрения ARIA пользователи все равно жалуются?
- Какой минимальный чек перед релизом сложного виджета?
- Итоги
Введение
Это статья с узким фокусом: здесь разбираем именно ARIA-атрибуты в React. Если нужен полный базовый контекст по доступности (семантика, клавиатура, фокус и типичные ошибки), начните с React accessibility (a11y) основы.
Тема ARIA в React всплывает почти в каждом проекте, где интерфейс выходит за пределы обычной формы из input и button. Пока команда использует только нативные элементы, доступность поддерживается на хорошем уровне автоматически. Как только появляются кастомные селекты, модалки, табы и сложные карточки с интерактивным поведением, семантика начинает ломаться, а вместе с ней ломается и путь пользователя.
На практике это выглядит одинаково: UI визуально работает, но человек с клавиатурой не может пройти сценарий до конца, а screen reader озвучивает хаотичный набор элементов. Похожая проблема часто возникает в системах с обилием абстракций и переиспользуемых примитивов, о чем хорошо видно на примерах из compound components в React.
В этой статье разберем ARIA-атрибуты в React с инженерной точки зрения: где граница между нативной семантикой и ARIA, как не сломать доступность в реальных компонентах, какие ошибки чаще всего уезжают в прод и как объяснить свои решения на интервью.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью
ARIA в React: где нужна, а где только мешает
Главный принцип прост: ARIA не заменяет HTML-семантику, а дополняет ее. Если элемент можно реализовать с помощью нативного тега, это почти всегда предпочтительнее. button, a, label, fieldset, table уже несут понятные роли и клавиатурное поведение. Когда команда вместо них использует div, она сначала удаляет встроенную доступность, а потом пытается восстановить её вручную.
Правило no ARIA is better than bad ARIA
Некорректное использование ARIA опаснее, чем ее полное отсутствие. Если вы укажете неверный role или конфликтующее состояние (aria-hidden на фокусируемом элементе, aria-expanded без контролируемой области), вспомогательные технологии получат противоречивую картину и начнут вести себя непредсказуемо.
Рабочий порядок такой:
- Выбрать правильный нативный элемент.
- Проверить клавиатурный сценарий.
- Добавить ARIA только для уточнения названия, состояния или связи между элементами.
Какие группы атрибутов используются чаще всего
В React-проектах чаще всего нужны три группы:
- Атрибуты названия:
aria-label,aria-labelledby,aria-describedby. - Атрибуты состояния:
aria-expanded,aria-selected,aria-pressed,aria-invalid,aria-busy. - Атрибуты отношений и регионов:
aria-controls,aria-live,aria-modal.
Самая частая ошибка middle-уровня: одновременно ставить aria-label и aria-labelledby без четкого приоритета и потом не понимать, какой текст реально озвучивается. Если есть видимый текст на странице, лучше использовать aria-labelledby, чтобы визуальный и озвучиваемый контент не расходились.
Архитектура доступности: слой семантики поверх дизайн-системы
Если у проекта больше 20-30 экранов, ARIA нельзя внедрять как точечный фикс. Нужен архитектурный слой доступности внутри дизайн-системы.
Схема компонентов
Базовая модель обычно выглядит так:
- UI-примитивы (
Button,Input,Dialog,Tabs) отвечают за корректный HTML и минимальный набор ARIA. - Feature-компоненты (
CheckoutForm,FiltersPanel) управляют бизнес-состояниями и передают семантически корректные пропсы. - Страницы собирают поток фокуса и порядок взаимодействия между блоками.
Поток управления
Событие пользователя идет сверху вниз: нажатие клавиши или клик меняет состояние компонента, компонент обновляет DOM, после чего assistive technology считывает новое состояние. Если на этом пути есть разрыв (например, визуально вкладка активна, но aria-selected не изменился), пользователь воспринимает не то, что визуально отображается.
Границы ответственности
Хорошая граница такая:
- Примитив не знает доменную логику, но гарантирует доступное поведение.
- Feature-слой не переопределяет семантику примитива произвольно.
- Любые переопределения ARIA-атрибутов проходят через ревью как изменение контракта компонента.
Узкие места и отказоустойчивость
Критическая точка отказа обычно одна: фокус. Если после закрытия модалки фокус не возвращается, пользователь теряет контекст. Если после ошибки формы фокус остается наверху страницы, пользователь может не заметить причину сбоя. Для деградации нужна простая стратегия: при сомнении откатиться к нативному элементу и минимальной семантике, а не создавать сложный кастомный виджет.
Подход с отдельным слоем UI-примитивов хорошо сочетается с рекомендациями из разбора архитектуры React-приложений: сначала контракт компонента, потом визуальные вариации.
Практика: рабочие ARIA-паттерны в React
Пример 1: иконка-кнопка с корректным названием
import { Trash2 } from "lucide-react";
type DeleteButtonProps = {
onDelete: () => void;
};
export function DeleteButton({ onDelete }: DeleteButtonProps) {
return (
<button
type="button"
onClick={onDelete}
aria-label="Удалить элемент"
className="icon-button"
>
<Trash2 aria-hidden="true" focusable="false" />
</button>
);
}
Здесь aria-label нужен, потому что у кнопки нет видимого текста. У SVG-иконки стоит aria-hidden="true", чтобы screen reader не озвучивал ее как отдельный элемент. Важно оставить именно button, а не div, иначе придется вручную эмулировать клавиатурное поведение.
Пример 2: поле формы с подсказкой и динамической ошибкой
import { useId } from "react";
type EmailFieldProps = {
value: string;
error?: string;
onChange: (value: string) => void;
};
export function EmailField({ value, error, onChange }: EmailFieldProps) {
const inputId = useId();
const hintId = `${inputId}-hint`;
const errorId = `${inputId}-error`;
const describedBy = [hintId, error ? errorId : null].filter(Boolean).join(" ");
return (
<div>
<label htmlFor={inputId}>Email</label>
<input
id={inputId}
type="email"
value={value}
onChange={(e) => onChange(e.target.value)}
aria-invalid={Boolean(error)}
aria-describedby={describedBy}
/>
<p id={hintId}>Используйте рабочую почту, чтобы получить приглашение.</p>
{error ? (
<p id={errorId} role="alert">
{error}
</p>
) : null}
</div>
);
}
Смысл паттерна в согласованности: label формирует имя поля, aria-describedby связывает поле с подсказкой и ошибкой, aria-invalid отражает статус в моменте. Это особенно важно для сценариев с контролируемыми формами и валидацией на лету, как в разборе controlled и uncontrolled компонентов.
Пример 3: модалка через portal с role="dialog"
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
type ModalProps = {
title: string;
open: boolean;
onClose: () => void;
children: React.ReactNode;
};
export function Modal({ title, open, onClose, children }: ModalProps) {
const dialogRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!open || !dialogRef.current) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
dialogRef.current.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
previouslyFocused?.focus();
};
}, [open, onClose]);
if (!open) return null;
return createPortal(
<div className="backdrop" onClick={onClose}>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={title}
tabIndex={-1}
className="dialog"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body
);
}
role="dialog" и aria-modal="true" сообщают тип контейнера, а возврат фокуса после закрытия сохраняет контекст пользователя. Без этого модалка формально откроется, но будет трудна в использовании. Для сложных случаев с вложенными слоями стоит держать в голове ограничения порталов, которые разобраны в материале про React Portals.
Таблица выбора: нативный HTML, headless-компоненты или кастом
| Критерий | Нативные элементы | Headless UI/библиотеки | Кастом на div + role |
|---|---|---|---|
| Скорость старта | Высокая | Средняя | Средняя |
| Риск ошибок доступности | Низкий | Средний | Высокий |
| Гибкость визуала | Средняя | Высокая | Высокая |
| Объем ручной ARIA-логики | Низкий | Средний | Очень высокий |
| Поддержка клавиатуры | Почти готова из коробки | Частично готова | Нужно писать вручную |
| Цена изменений в проде | Низкая | Средняя | Высокая |
| Когда выбирать | Простые формы и действия | Сложные паттерны с быстрой окупаемостью разработки | Только если есть сильная команда и жесткие ограничения |
Практический вывод: кастомная реализация с div и ARIA оправдана редко. Чаще она появляется не из-за требований продукта, а из-за раннего решения "нарисовать свой контрол с нуля", которое потом сложно поддерживать.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks
Типичные проблемы в реальных проектах: что ломается после релиза
1. Неполная синхронизация состояния
Симптом: компонент визуально "открыт", но aria-expanded остался false. В логах ошибок это почти не видно, зато в пользовательских жалобах появляется формулировка "ничего не происходит". Устраняется с помощью контрактных тестов на соответствие визуального и ARIA-состояния.
2. Потеря фокуса при условном рендеринге
Симптом: после submit или закрытия диалога фокус перемещается на body. Пользователь с клавиатурой теряет позицию и вынужден проходить экран заново. Раннее обнаружение: сквозные (e2e) тесты, где явно проверяется document.activeElement после ключевых действий.
3. "Шумные" обновления динамической области
Симптом: aria-live озвучивает каждое мелкое изменение, интерфейс перегружает пользователя лишними объявлениями. Последствие: полезные сообщения теряются среди второстепенных. Решается с помощью дебаунса событий и правила "озвучивать только значимые переходы состояния".
4. Переопределение роли без бизнес-причины
Симптом: элементы получают role ради прохождения линтера, а не ради пользователя. Через несколько спринтов такая разметка перестает соответствовать реальному поведению. Нужен процесс ревью, где изменение role рассматривается как изменение публичного API компонента.
5. Доступность проверяют только на уровне линтера
Симптом: eslint-plugin-jsx-a11y зеленый, но сценарий оформления заказа не проходится с клавиатуры. Автоматические проверки нужны, но без ручного сценария они не ловят последовательные ошибки фокуса и контекста.
Разбор производительности: где реальная цена ARIA
Сами ARIA-атрибуты почти не дают заметной нагрузки на CPU или память. Основная цена обычно появляется в другой плоскости: в том, как часто вы меняете состояние и насколько крупное дерево ререндерите.
Реальные узкие места:
- Частые перерисовки больших списков, где при каждом вводе обновляется множество элементов с динамическими
aria-*. - Live-region, в который без фильтрации отправляются десятки сообщений в секунду.
- Сложные виджеты с ручным focus management, где много обработчиков и синхронизации.
Что делать на практике:
- Профилировать сначала поток ререндеров, а не атрибуты.
- Стабилизировать props и декомпозировать крупные контейнеры.
- Для шумных областей ограничивать частоту анонсов.
Этот подход совпадает с общей методикой диагностики из гайда по React performance profiling: сначала источник ререндера, потом точечная оптимизация.
Практики команды: как поддерживать доступность без авралов
- Архитектура: держать ARIA-логику в UI-примитивах, а не размазывать по feature-слою.
- Код: запрещать интерактивные
divв линт-правилах, если нет документированного исключения. - Наблюдаемость: логировать критичные UI-события (открытие диалога, ошибки формы) и проверять путь фокуса в e2e.
- Тестирование: комбинировать unit/e2e,
axeи ручную проверку клавиатурной навигации на ключевых пользовательских сценариях. - Внедрение: выпускать изменения в сложных виджетах через функциональные флаги и иметь быстрый откат на предыдущий примитив.
Команды, которые системно делают эти пять шагов, реже получают регрессии доступности и быстрее проходят ревью по продуктовым фичам.
Частые ошибки
- Ставить
aria-labelтам, где уже есть хороший видимый текст и нуженaria-labelledby. - Использовать
role="button"как постоянную замену нативной кнопке. - Забывать возвращать фокус после закрытия модалки.
- Обновлять
aria-liveпри каждом символе ввода. - Считать, что линтер закрывает все риски.
- Добавлять ARIA ради "галочки", не проверяя реальное поведение со screen reader.
Многие из этих паттернов пересекаются с типичными проблемами компонентной архитектуры, которые часто всплывают и в списке React anti-patterns.
Как отвечать на интервью про ARIA в React
Сильный ответ обычно состоит из пяти блоков:
- База: "Я начинаю с нативной семантики, ARIA использую как уточняющий слой".
- Практика: "Для форм связываю label, hint и error через
aria-describedby, для ошибок использую корректные статусы". - Сложные виджеты: "В модалках контролирую фокус,
role=dialog,aria-modal, возврат фокуса после закрытия". - Контроль качества: "Проверяю линтером, автоматикой и ручным сценарием с клавиатурой и screen reader".
- Компромиссы: "Не пишу кастом на
div, если это можно решить нативным элементом".
Если хотите усилить ответ до уровня middle+, добавьте конкретный кейс из практики: что именно ломалось, как вы это обнаружили, какие метрики или пользовательские сигналы подсказали проблему и как вы предотвратили повтор.
Подготовься к React-собеседованиям на реальных инженерных кейсах
Тренируем разбор сложных React-вопросов: архитектура компонентов, доступность, производительность и аргументация решений без шаблонных ответов
FAQ
Нужно ли добавлять ARIA почти в каждый React-компонент?
Нет. Сначала используйте правильный HTML и проверяйте поведение по клавиатуре. ARIA нужен там, где стандартная семантика не покрывает сценарий полностью.
Что выбрать: aria-label или aria-labelledby?
aria-labelledby обычно приоритетнее, если на экране уже есть видимый текст. aria-label подходит для элементов без визуального названия, например для кнопки только с иконкой.
Можно ли сделать доступный интерфейс только на UI-библиотеке?
Библиотека дает хороший старт, но не решает доменные сценарии за вас. Названия, ошибки, фокус между шагами и контекст конкретного продукта остаются задачей команды.
Почему после внедрения ARIA пользователи все равно жалуются?
Чаще всего проблема не в отсутствии атрибутов, а в несогласованности поведения: неверный порядок фокуса, расхождение визуального и озвучиваемого состояния, шумные live-region сообщения.
Какой минимальный чек перед релизом сложного виджета?
Линтер + автопроверка axe + ручной проход с клавиатурой + короткая базовая проверка со скринридером на ключевом пользовательском сценарии.
Итоги
ARIA атрибуты в React эффективны, когда команда воспринимает их как часть архитектуры, а не как косметическую правку после готового UI. Нативная семантика должна быть базой, ARIA - точным инструментом для названий, состояний и связей, а качество проверяется не только по коду, но и по реальному поведению интерфейса.
Если закрепить этот подход в дизайн-системе и процессе ревью, доступность перестает быть "разовой задачей перед релизом" и становится обычным инженерным стандартом, который масштабируется вместе с продуктом.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью
Автор
Lexicon Team
Читайте также
frontend
React accessibility (a11y) основы: семантика, клавиатура, ARIA и типичные ошибки
Разбираем основы React accessibility: как строить доступные компоненты, когда нужны ARIA-атрибуты, как работать с фокусом, формами и модалками и что отвечать на собеседовании.
frontend
React data/state: 17 сложных вопросов с объяснением для собеседования
17 сложных вопросов по React data/state: query key, invalidation, optimistic update, Zustand, URL state, drafts форм и сильные ответы для middle-интервью.
frontend
15 сложных вопросов про React Profiler и диагностику performance
Разбираем 15 сложных вопросов про React Profiler: commit, flamegraph, ranked view, bottleneck, ререндеры, browser Performance и сильные ответы на интервью.