Семантика и доступность

Семантика пользовательского интерфейса — это способ, которым элементы интерфейса передают своё значение браузеру, вспомогательным технологиям и другим инструментам. В веб-разработке семантика формируется через корректное использование HTML-элементов, ролей ARIA, атрибутов состояния и правильной иерархии структуры документа.

Библиотека Radix UI построена вокруг принципа: поведение компонента не должно нарушать семантику HTML. Компоненты Radix реализуют доступные паттерны интерфейсов, но при этом сохраняют контроль разработчика над конечной разметкой.

Основная идея заключается в разделении:

  • логики и поведения компонента
  • семантической HTML-разметки
  • визуального оформления

Radix реализует поведение и доступность, не навязывая конкретные теги или стили.


Доступность как архитектурный принцип

Большинство UI-библиотек решают доступность постфактум. В Radix доступность заложена в архитектуру компонентов.

Компоненты реализуют:

  • управление focus-навигацией
  • корректные ARIA-атрибуты
  • поддержку screen reader
  • управление keyboard navigation
  • правильную иерархию ролей

Это означает, что элементы интерфейса ведут себя так же, как их нативные аналоги.

Например:

  • меню открывается клавишами Enter и Space
  • закрытие происходит по Escape
  • перемещение между элементами выполняется стрелками
  • фокус возвращается к триггеру после закрытия

Все эти правила реализованы внутри компонентов.


Headless-подход и сохранение семантики

Radix UI реализует headless-компоненты. Это означает, что библиотека не предоставляет готовый HTML-интерфейс, а лишь управляет состоянием и поведением.

Вместо готовых компонентов вроде:

<Menu />

используется композиционная модель:

<Menu.Root>
  <Menu.Trigger />
  <Menu.Content>
    <Menu.Item />
  </Menu.Content>
</Menu.Root>

Каждый элемент отвечает за конкретную часть поведения.

Такая архитектура позволяет:

  • контролировать HTML-теги
  • изменять семантику
  • переиспользовать компоненты
  • избегать лишней вложенности

Механизм 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 используется переданный элемент.

Это обеспечивает:

  • контроль семантики
  • отсутствие лишних DOM-узлов
  • интеграцию с собственными компонентами

ARIA-атрибуты в Radix UI

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.Titlearia-labelledby
  • Dialog.Descriptionaria-describedby

Screen reader получает корректное описание диалога.


Управление фокусом

Фокусировка — один из самых сложных аспектов доступности.

Radix реализует полноценную систему управления фокусом:

Основные правила

При открытии диалога:

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

При закрытии:

  • фокус возвращается к Trigger

Это предотвращает ситуацию, когда пользователь клавиатуры «теряет» фокус.


Focus Trap

Компоненты вроде Dialog, Popover и DropdownMenu используют focus trap.

Это означает:

  • Tab перемещает фокус только внутри компонента
  • невозможно выйти за пределы интерфейса

Пример:

[ input ] -> [ button ] -> [ close ] -> снова [ input ]

Фокус циклически перемещается внутри интерфейса.


Keyboard Navigation

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>

Навигация по пунктам выполняется стрелками.


Роли (ARIA Roles)

Каждый компонент 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

Связывание элементов через ID

ARIA-атрибуты часто требуют уникальных id.

Radix автоматически генерирует их.

Например:

aria-controls="radix-:R1:"
aria-labelledby="radix-:R2:"

Это обеспечивает:

  • уникальность
  • корректные связи
  • отсутствие конфликтов

Управление состоянием через ARIA

Состояние компонентов отражается в ARIA-атрибутах.

Примеры:

aria-expanded
aria-checked
aria-selected
aria-disabled

Когда меню открыто:

aria-expanded="true"

Когда закрыто:

aria-expanded="false"

Screen reader сообщает пользователю состояние интерфейса.


Доступность для screen reader

Radix оптимизирован для работы с популярными screen reader:

  • NVDA
  • JAWS
  • VoiceOver
  • TalkBack

Это достигается через:

  • корректные роли
  • описания
  • live-обновления
  • управление фокусом

Например, при открытии диалога screen reader произносит:

"Диалог. Настройки. Изменение параметров профиля"

Оверлеи и скрытие контента

При открытии модальных компонентов необходимо скрывать остальной интерфейс.

Radix реализует это через:

aria-hidden

Контент вне диалога получает:

aria-hidden="true"

Это означает, что screen reader игнорирует его.

Пользователь слышит только содержимое диалога.


Portal и доступность

Компоненты вроде Dialog и Popover используют Portal.

Это переносит элемент в конец body.

<body>
  <div id="app"></div>

  <div data-radix-portal>
    dialog content
  </div>
</body>

Преимущества:

  • отсутствие проблем со z-index
  • изоляция оверлеев
  • корректная структура DOM

При этом доступность не нарушается, так как ARIA-связи сохраняются.


Disabled-состояния

Radix поддерживает корректную работу отключённых элементов.

Пример:

<Menu.Item disabled>
  Удалить
</Menu.Item>

В DOM это превращается в:

aria-disabled="true"
data-disabled

Особенности:

  • элемент не получает фокус
  • клавиатура пропускает его
  • screen reader сообщает состояние

Data-атрибуты состояния

Radix активно использует data-* атрибуты.

Примеры:

data-state="open"
data-state="closed"
data-disabled
data-highlighted

Это даёт два преимущества:

1. Управление стилями

[data-state="open"] {
  opacity: 1;
}

2. Семантическая прозрачность

DOM ясно показывает состояние компонента.


Проверка доступности

При использовании Radix UI интерфейс автоматически соответствует большинству требований:

  • WCAG
  • WAI-ARIA Authoring Practices

Тем не менее рекомендуется проверять интерфейс через:

  • Lighthouse
  • axe DevTools
  • screen reader

Проверяются:

  • фокус
  • навигация клавиатурой
  • aria-связи
  • читаемость

Семантические ошибки, которых избегает Radix

Неправильная вложенность интерактивных элементов

Плохой пример:

<button>
  <a>link</a>
</button>

Radix предотвращает такие ситуации через asChild.


Потеря фокуса

Без focus management пользователь может оказаться вне интерфейса.

Radix возвращает фокус автоматически.


Неправильные роли

Многие библиотеки используют div вместо семантических элементов.

Radix добавляет корректные ARIA-роли.


Отсутствие keyboard navigation

Некоторые интерфейсы работают только мышью.

Radix реализует полную поддержку клавиатуры.


Архитектурные преимущества Radix для доступности

Radix обеспечивает доступность через несколько уровней архитектуры.

1. Primitive-компоненты

Низкоуровневые элементы:

Primitive.button
Primitive.div

Используются для управления DOM.


2. Accessible behaviors

Каждый компонент реализует:

  • фокус
  • aria
  • клавиатуру

3. Controlled state

Состояние можно контролировать:

open
defaultOpen
onOpenChange

Это важно для синхронизации интерфейса.


4. Композиционная модель

Поведение собирается из маленьких частей:

Root
Trigger
Content
Item

Каждая часть имеет собственную семантику.


Практическая модель доступного интерфейса

Типичная структура доступного Radix-компонента включает:

  1. Trigger — элемент открытия
  2. Content — содержимое
  3. Focus management
  4. ARIA roles
  5. Keyboard navigation

Пример:

<Popover.Root>

  <Popover.Trigger>
    Показать информацию
  </Popover.Trigger>

  <Popover.Content>
    Дополнительные сведения
  </Popover.Content>

</Popover.Root>

Radix автоматически добавляет:

aria-expanded
aria-controls
role
focus trap

В результате интерфейс:

  • доступен клавиатуре
  • корректно озвучивается
  • соответствует стандартам WAI-ARIA
  • сохраняет семантику HTML.