Семантика пользовательского интерфейса — это способ, которым элементы интерфейса передают своё значение браузеру, вспомогательным технологиям и другим инструментам. В веб-разработке семантика формируется через корректное использование HTML-элементов, ролей ARIA, атрибутов состояния и правильной иерархии структуры документа.
Библиотека Radix UI построена вокруг принципа: поведение компонента не должно нарушать семантику HTML. Компоненты Radix реализуют доступные паттерны интерфейсов, но при этом сохраняют контроль разработчика над конечной разметкой.
Основная идея заключается в разделении:
Radix реализует поведение и доступность, не навязывая конкретные теги или стили.
Большинство UI-библиотек решают доступность постфактум. В Radix доступность заложена в архитектуру компонентов.
Компоненты реализуют:
Это означает, что элементы интерфейса ведут себя так же, как их нативные аналоги.
Например:
Enter и
SpaceEscapeВсе эти правила реализованы внутри компонентов.
Radix UI реализует headless-компоненты. Это означает, что библиотека не предоставляет готовый HTML-интерфейс, а лишь управляет состоянием и поведением.
Вместо готовых компонентов вроде:
<Menu />
используется композиционная модель:
<Menu.Root>
<Menu.Trigger />
<Menu.Content>
<Menu.Item />
</Menu.Content>
</Menu.Root>
Каждый элемент отвечает за конкретную часть поведения.
Такая архитектура позволяет:
asChildОдной из ключевых особенностей Radix является проп
asChild.
Он позволяет заменить внутренний HTML-элемент на любой другой.
Пример:
import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<button className="open-button">
Открыть диалог
</button>
</Dialog.Trigger>
Без asChild компонент создаёт собственный элемент:
<button>
С asChild используется переданный элемент.
Это обеспечивает:
Radix автоматически управляет ARIA-атрибутами, необходимыми для доступности.
Например, компонент Dialog создаёт следующие
атрибуты:
aria-labelledby
aria-describedby
aria-modal
role="dialog"
Пример структуры:
<Dialog.Root>
<Dialog.Trigger>Открыть</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Настройки</Dialog.Title>
<Dialog.Description>
Изменение параметров профиля
</Dialog.Description>
<Dialog.Close>Закрыть</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Radix автоматически связывает:
Dialog.Title → aria-labelledbyDialog.Description → aria-describedbyScreen reader получает корректное описание диалога.
Фокусировка — один из самых сложных аспектов доступности.
Radix реализует полноценную систему управления фокусом:
При открытии диалога:
При закрытии:
TriggerЭто предотвращает ситуацию, когда пользователь клавиатуры «теряет» фокус.
Компоненты вроде Dialog, Popover и
DropdownMenu используют focus trap.
Это означает:
Tab перемещает фокус только внутри компонентаПример:
[ input ] -> [ button ] -> [ close ] -> снова [ input ]
Фокус циклически перемещается внутри интерфейса.
Radix реализует стандартные паттерны клавиатурной навигации, определённые спецификациями WAI-ARIA.
Поддерживаемые клавиши:
| Клавиша | Поведение |
|---|---|
| Enter | открывает меню |
| Space | открывает меню |
| ArrowDown | переход к следующему пункту |
| ArrowUp | переход к предыдущему |
| Escape | закрытие |
Пример:
<DropdownMenu.Root>
<DropdownMenu.Trigger>
Открыть меню
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>
Профиль
</DropdownMenu.Item>
<DropdownMenu.Item>
Настройки
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
Навигация по пунктам выполняется стрелками.
Каждый компонент Radix имеет корректную ARIA-роль.
Примеры:
| Компонент | ARIA роль |
|---|---|
| Dialog | dialog |
| DropdownMenu | menu |
| Tabs | tablist |
| Tab | tab |
| Tooltip | tooltip |
| Slider | slider |
Например, Tabs создаёт структуру:
tablist
├ tab
├ tab
└ tab
Пример:
<Tabs.Root defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1">
Профиль
</Tabs.Trigger>
<Tabs.Trigger value="tab2">
Безопасность
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">
...
</Tabs.Content>
<Tabs.Content value="tab2">
...
</Tabs.Content>
</Tabs.Root>
Radix автоматически добавляет:
role="tablist"
role="tab"
aria-selected
aria-controls
ARIA-атрибуты часто требуют уникальных id.
Radix автоматически генерирует их.
Например:
aria-controls="radix-:R1:"
aria-labelledby="radix-:R2:"
Это обеспечивает:
Состояние компонентов отражается в ARIA-атрибутах.
Примеры:
aria-expanded
aria-checked
aria-selected
aria-disabled
Когда меню открыто:
aria-expanded="true"
Когда закрыто:
aria-expanded="false"
Screen reader сообщает пользователю состояние интерфейса.
Radix оптимизирован для работы с популярными screen reader:
Это достигается через:
Например, при открытии диалога screen reader произносит:
"Диалог. Настройки. Изменение параметров профиля"
При открытии модальных компонентов необходимо скрывать остальной интерфейс.
Radix реализует это через:
aria-hidden
Контент вне диалога получает:
aria-hidden="true"
Это означает, что screen reader игнорирует его.
Пользователь слышит только содержимое диалога.
Компоненты вроде Dialog и Popover
используют Portal.
Это переносит элемент в конец body.
<body>
<div id="app"></div>
<div data-radix-portal>
dialog content
</div>
</body>
Преимущества:
z-indexПри этом доступность не нарушается, так как ARIA-связи сохраняются.
Radix поддерживает корректную работу отключённых элементов.
Пример:
<Menu.Item disabled>
Удалить
</Menu.Item>
В DOM это превращается в:
aria-disabled="true"
data-disabled
Особенности:
Radix активно использует data-* атрибуты.
Примеры:
data-state="open"
data-state="closed"
data-disabled
data-highlighted
Это даёт два преимущества:
[data-state="open"] {
opacity: 1;
}
DOM ясно показывает состояние компонента.
При использовании Radix UI интерфейс автоматически соответствует большинству требований:
Тем не менее рекомендуется проверять интерфейс через:
Проверяются:
Плохой пример:
<button>
<a>link</a>
</button>
Radix предотвращает такие ситуации через asChild.
Без focus management пользователь может оказаться вне интерфейса.
Radix возвращает фокус автоматически.
Многие библиотеки используют div вместо семантических
элементов.
Radix добавляет корректные ARIA-роли.
Некоторые интерфейсы работают только мышью.
Radix реализует полную поддержку клавиатуры.
Radix обеспечивает доступность через несколько уровней архитектуры.
Низкоуровневые элементы:
Primitive.button
Primitive.div
Используются для управления DOM.
Каждый компонент реализует:
Состояние можно контролировать:
open
defaultOpen
onOpenChange
Это важно для синхронизации интерфейса.
Поведение собирается из маленьких частей:
Root
Trigger
Content
Item
Каждая часть имеет собственную семантику.
Типичная структура доступного Radix-компонента включает:
Пример:
<Popover.Root>
<Popover.Trigger>
Показать информацию
</Popover.Trigger>
<Popover.Content>
Дополнительные сведения
</Popover.Content>
</Popover.Root>
Radix автоматически добавляет:
aria-expanded
aria-controls
role
focus trap
В результате интерфейс: