Radix UI предоставляет набор готовых примитивов для построения пользовательских интерфейсов, однако иногда возникает необходимость создавать собственные примитивы с уникальной логикой и поведением. Собственные примитивы позволяют сохранить консистентность интерфейса, управлять состоянием компонентов и интегрировать сторонние библиотеки.
Примитив в Radix UI состоит из нескольких ключевых элементов:
Для демонстрации создадим собственный примитив переключателя состояния.
import React, { createContext, useContext, useState } from "react";
const ToggleContext = createContext();
export function ToggleRoot({ children, defaultOpen = false }) {
const [open, setOpen] = useState(defaultOpen);
return (
<ToggleContext.Provider value={{ open, setOpen }}>
{children}
</ToggleContext.Provider>
);
}
export function ToggleTrigger({ children }) {
const { open, setOpen } = useContext(ToggleContext);
return (
<button onCl ick={() => setOpen(!open)}>
{children} {open ? "Включено" : "Выключено"}
</button>
);
}
export function ToggleContent({ children }) {
const { open } = useContext(ToggleContext);
if (!open) return null;
return <div>{children}</div>;
}
В этом примере:
open
через useState и предоставляет его дочерним компонентам
через контекст.open истинно.Для сложных интерфейсов с модальными окнами или всплывающими подсказками необходимо использовать порталы, чтобы избежать проблем с CSS:
import { createPortal } from "react-dom";
export function TogglePortal({ children }) {
return createPortal(
<div className="toggle-portal">{children}</div>,
document.body
);
}
Интеграция портала в примитив позволяет выводить контент поверх других элементов, сохраняя логику управления состоянием через контекст.
Для примитивов с несколькими уровнями взаимодействия удобно использовать compound components с контекстом:
const DropdownContext = createContext();
export function Dropdown({ children }) {
const [open, setOpen] = useState(false);
return (
<DropdownContext.Provider value={{ open, setOpen }}>
<div className="dropdown">{children}</div>
</DropdownContext.Provider>
);
}
export function DropdownTrigger({ children }) {
const { setOpen } = useContext(DropdownContext);
return <button onCl ick={() => setOpen(prev => !prev)}>{children}</button>;
}
export function DropdownContent({ children }) {
const { open } = useContext(DropdownContext);
return open ? <div className="dropdown-content">{children}</div> : null;
}
Ключевые моменты:
Radix UI позволяет создавать собственные примитивы на основе существующих, расширяя их функционал:
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
export function CustomCheckbox({ label, ...props }) {
return (
<CheckboxPrimitive.Root {...props}>
<CheckboxPrimitive.Indicator />
{label && <span>{label}</span>}
</CheckboxPrimitive.Root>
);
}
Расширение стандартного примитива сохраняет совместимость с API Radix и позволяет добавлять кастомную визуализацию и логику.
Каждый собственный примитив должен учитывать accessibility (ARIA) атрибуты, фокусировку и keyboard navigation. Примитивы Radix UI предоставляют удобные хуки и вспомогательные компоненты для этих целей, которые можно интегрировать в собственные примитивы.
<button aria-pressed={open}>Toggle</button>
Правильная работа с ARIA и фокусом обеспечивает совместимость с экранными читалками и улучшает UX для пользователей с ограниченными возможностями.
Создание собственных примитивов в Radix UI строится вокруг концепции управления состоянием через контекст, разделения на root, trigger и content, и применения порталов для визуальных компонентов. Такой подход обеспечивает гибкость, переиспользуемость и соответствие стандартам доступности.