Base UI

Base UI - библиотека headless-компонентов от команды MUI. Предоставляет логику, состояние и доступность без какой-либо стилизации. Компоненты можно стилизовать любым CSS-решением: Tailwind CSS, CSS Modules, styled-components, Emotion или обычным CSS.

Установка

npm install @base-ui-components/react

Headless-подход

Headless-компоненты содержат логику взаимодействия, управление фокусом, ARIA-атрибуты и keyboard navigation, но не включают визуальных стилей. Разработчик получает полный контроль над внешним видом, сохраняя корректное поведение и доступность.

Основные компоненты

Button

import { Button } from '@base-ui-components/react/button';
 
function ButtonExample() {
  return (
    <Button
      className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-600 disabled:opacity-50"
      onClick={() => console.log('clicked')}
    >
      Нажми
    </Button>
  );
}
import { Menu } from '@base-ui-components/react/menu';
 
function MenuExample() {
  return (
    <Menu.Root>
      <Menu.Trigger className="rounded-md border px-3 py-2 hover:bg-gray-50">
        Действия
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner>
          <Menu.Popup className="rounded-lg border bg-white p-1 shadow-lg">
            <Menu.Item
              className="cursor-pointer rounded-md px-3 py-2 hover:bg-gray-100"
              onSelect={() => console.log('edit')}
            >
              Редактировать
            </Menu.Item>
            <Menu.Item
              className="cursor-pointer rounded-md px-3 py-2 hover:bg-gray-100"
              onSelect={() => console.log('duplicate')}
            >
              Дублировать
            </Menu.Item>
            <Menu.Separator className="my-1 h-px bg-gray-200" />
            <Menu.Item
              className="cursor-pointer rounded-md px-3 py-2 text-red-600 hover:bg-red-50"
              onSelect={() => console.log('delete')}
            >
              Удалить
            </Menu.Item>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  );
}

Dialog

import { Dialog } from '@base-ui-components/react/dialog';
 
function DialogExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="rounded-md bg-blue-600 px-4 py-2 text-white">
        Открыть
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
        <Dialog.Popup className="fixed left-1/2 top-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-xl bg-white p-6 shadow-xl">
          <Dialog.Title className="text-lg font-semibold">
            Подтверждение
          </Dialog.Title>
          <Dialog.Description className="mt-2 text-gray-600">
            Вы уверены, что хотите продолжить? Это действие нельзя отменить.
          </Dialog.Description>
          <div className="mt-6 flex justify-end gap-3">
            <Dialog.Close className="rounded-md border px-4 py-2 hover:bg-gray-50">
              Отмена
            </Dialog.Close>
            <button className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700">
              Удалить
            </button>
          </div>
        </Dialog.Popup>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Tooltip

import { Tooltip } from '@base-ui-components/react/tooltip';
 
function TooltipExample() {
  return (
    <Tooltip.Provider>
      <Tooltip.Root>
        <Tooltip.Trigger className="rounded-md border px-3 py-2">
          Наведи
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Positioner>
            <Tooltip.Popup className="rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white shadow-md">
              Подсказка с полезной информацией
              <Tooltip.Arrow className="fill-gray-900" />
            </Tooltip.Popup>
          </Tooltip.Positioner>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
}

Switch

import { Switch } from '@base-ui-components/react/switch';
 
function SwitchExample() {
  const [checked, setChecked] = useState(false);
 
  return (
    <label className="flex items-center gap-3">
      <Switch.Root
        checked={checked}
        onCheckedChange={setChecked}
        className="relative h-6 w-11 rounded-full bg-gray-300 transition-colors data-[checked]:bg-blue-600"
      >
        <Switch.Thumb className="block h-5 w-5 translate-x-0.5 rounded-full bg-white shadow transition-transform data-[checked]:translate-x-[22px]" />
      </Switch.Root>
      <span>Уведомления</span>
    </label>
  );
}

Select

import { Select } from '@base-ui-components/react/select';
 
interface Option {
  value: string;
  label: string;
}
 
const options: Option[] = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
  { value: 'svelte', label: 'Svelte' },
];
 
function SelectExample() {
  return (
    <Select.Root>
      <Select.Trigger className="flex items-center justify-between rounded-md border px-3 py-2 w-48">
        <Select.Value placeholder="Выберите фреймворк" />
        <Select.Icon>
          <ChevronDownIcon className="h-4 w-4" />
        </Select.Icon>
      </Select.Trigger>
      <Select.Portal>
        <Select.Positioner>
          <Select.Popup className="rounded-lg border bg-white p-1 shadow-lg">
            {options.map((option) => (
              <Select.Item
                key={option.value}
                value={option.value}
                className="cursor-pointer rounded-md px-3 py-2 hover:bg-gray-100 data-[highlighted]:bg-gray-100"
              >
                <Select.ItemText>{option.label}</Select.ItemText>
                <Select.ItemIndicator>
                  <CheckIcon className="h-4 w-4" />
                </Select.ItemIndicator>
              </Select.Item>
            ))}
          </Select.Popup>
        </Select.Positioner>
      </Select.Portal>
    </Select.Root>
  );
}

Стилизация с различными CSS-решениями

CSS Modules

/* Dialog.module.css */
.backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
}
 
.popup {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: white;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
import styles from './Dialog.module.css';
 
<Dialog.Backdrop className={styles.backdrop} />
<Dialog.Popup className={styles.popup}>...</Dialog.Popup>

Styled Components

import styled from 'styled-components';
import { Dialog } from '@base-ui-components/react/dialog';
 
const StyledBackdrop = styled(Dialog.Backdrop)`
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(4px);
`;
 
const StyledPopup = styled(Dialog.Popup)`
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: white;
  border-radius: 12px;
  padding: 24px;
`;

Сравнение с Headless UI и Radix

АспектBase UIHeadless UIRadix UI
КомандаMUITailwind LabsWorkOS
Компоненты25+10+30+
СтилизацияЛюбаяTailwind-firstЛюбая
АнимацииCSS/JSВстроенные transitionsCSS/JS
Размер бандлаМинимальныйМинимальныйМинимальный
ТипизацияTypeScriptTypeScriptTypeScript

Radix UI более зрелый проект с большим числом компонентов. Именно Radix лежит в основе shadcn/ui. Base UI сильнее интегрирован с экосистемой MUI и может быть предпочтительнее для команд, использующих MUI в других проектах.

Headless UI ориентирован на Tailwind CSS и предоставляет меньше компонентов, зато с более простым API и встроенными transition-анимациями.

Когда использовать Base UI вместо MUI

Base UI подходит в следующих случаях:

  • Проект имеет уникальный дизайн, не основанный на Material Design
  • Нужен минимальный размер бандла
  • Команда хочет полный контроль над стилями
  • Используется Tailwind CSS или другой CSS-фреймворк
  • Нужна accessible-основа для собственной дизайн-системы

MUI лучше подходит, когда:

  • Нужны готовые стилизованные компоненты с Material Design
  • Важна скорость разработки без кастомной стилизации
  • Нужны продвинутые компоненты вроде DataGrid
  • Команда знакома с Material Design

Base UI

Base UI занимает нишу между полноценными UI-фреймворками и написанием компонентов с нуля. Библиотека решает самые сложные проблемы - доступность, управление фокусом, keyboard navigation - оставляя визуальное оформление разработчику. Хороший выбор для команд, строящих собственную дизайн-систему.