Урок 16. Теория: реестр, примитивы, цена владения

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~40 мин 🎯 Сложность: Средняя

⚡ Кратко

Обычная библиотека компонентов — зависимость: вы получаете готовое поведение и внешний вид, но меняете его в пределах, которые предусмотрел автор. shadcn/ui отдаёт исходники: поведение приходит из примитивов, оформление — из Tailwind, а код принадлежит вам. Плата — вы сами обновляете компоненты и сами отвечаете за их качество.

Реестр вместо пакета

Установка обычной библиотеки выглядит так: npm i ui-kit, затем import { Button } from "ui-kit". Код кнопки живёт в node_modules и вам не принадлежит.

shadcn/ui работает иначе:

npx shadcn@latest add button

Команда скачивает описание компонента из реестра, ставит нужные зависимости и кладёт файл в ваш проект:

src/
├── components/
│   └── ui/
│       └── button.tsx   ← ваш файл, его можно править
└── lib/
    └── utils.ts         ← функция cn()

После этого пакета shadcn/ui в зависимостях нет. Есть обычный React-компонент, который вы читаете, правите и коммитите вместе с остальным кодом.

Библиотека компонентов — готовая мебель из магазина: собрал и пользуйся, но перекрасить нельзя, а если ножка не подходит — терпи. shadcn/ui — набор чертежей и фурнитуры: стол приезжает собранным, но чертёж у вас на руках, и укоротить ножку можно в любой момент. Взамен вы отвечаете за то, чтобы стол не развалился.

Три слоя компонента

СлойКто отвечаетЧто делает
Поведение и доступностьпримитивы (Base UI, опционально Radix)фокус-ловушка, клавиатура, ARIA, порталы, позиционирование
ОформлениеTailwind + cvaварианты, размеры, состояния
Сборкаваш файл в components/uiсклеивает первое со вторым

Именно поэтому исходник компонента такой короткий: тяжёлую работу (управление фокусом, обработку клавиш, ARIA-атрибуты) делает примитив, а файл в вашем проекте отвечает только за внешний вид и композицию.

// так выглядит настоящий Button из реестра (сокращённо)
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva("inline-flex items-center …", {
  variants: { variant: { default: "bg-primary …", outline: "border-border …" } },
})

function Button({ className, variant, size, ...props }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
  return <ButtonPrimitive data-slot="button" className={{cn(buttonVariants({{ variant, size }}), className)}} {{...props}} />
}

Что даёт «владение кодом»

  • Правки без борьбы. Нужен пятый вариант кнопки — добавляете строчку в cva, а не ищете, как перебить чужие стили.
  • Нет неожиданных изменений. Обновление библиотеки не может внезапно изменить вид вашего интерфейса: компоненты обновляете вы.
  • Понятная отладка. Ошибку видно в вашем файле, а не в минифицированном коде из node_modules.
  • Меньше зависимостей в бандле. В проект попадает только то, что вы добавили.

Чем за это платят

  • Обновления вручную. Исправили баг в реестре — нужно повторно добавить компонент и посмотреть diff.
  • Ответственность за качество. Скопировали — значит, доступность и поведение теперь ваша забота.
  • Дисциплина в команде. Ничто не мешает пятерым разработчикам развести пять разных кнопок.
  • Порог входа. Нужно понимать Tailwind, cva и примитивы — то есть всё, что было в первой половине курса.
Главное заблуждение новичка: «shadcn/ui — это библиотека, которая сама всё оформит». Нет. Это отправная точка, которую вы дальше ведёте сами. Если в команде нет ресурса на поддержку интерфейсной части — готовая библиотека честнее.

Сравнение подходов

Библиотека (MUI, Chakra, Ant)shadcn/ui
Где живёт кодnode_modulesваш репозиторий
Кастомизациячерез API темы и переопределенияправкой исходника
Обновленияnpm updateповторное добавление, ручной diff
Риск сломать дизайнпри мажорном обновлениитолько когда вы сами обновите
Скорость стартаочень высокаявысокая
Единство в командеобеспечено библиотекойобеспечивается дисциплиной
Размер бандлазависит от tree-shakingтолько добавленные компоненты

Доступность из коробки

Самая недооценённая часть. Примитив берёт на себя то, что почти никто не реализует правильно вручную:

  • ловушка фокуса в диалоге и возврат фокуса при закрытии;
  • навигация стрелками в меню, вкладках и списках;
  • закрытие по Esc и клику вне;
  • корректные aria-* и связи aria-labelledby;
  • отрисовка через портал, чтобы всплывающее не обрезалось родителем;
  • блокировка прокрутки страницы под модальным окном.
Проверьте сами: откройте любой компонент в документации и пройдите его только клавиатурой — Tab, стрелки, Esc. Потом попробуйте повторить это поведение в самописном меню. Разница в объёме работы огромна.

Выбор примитивов

Современный CLI умеет ставить компоненты на разных «базах»: по умолчанию Base UI, но доступны и Radix, и React Aria — флаг --base. Знать об этом полезно: множество статей и старых проектов написаны под Radix, и API там отличается (например, asChild вместо render).

npx shadcn@latest init --base base    # Base UI (по умолчанию)
npx shadcn@latest init --base radix   # Radix UI
npx shadcn@latest init --base aria    # React Aria

Когда shadcn/ui — не лучший выбор

  • Проект без Tailwind: смысла мало, компоненты оформлены утилитами.
  • Команда без фронтенд-ресурса: некому вести компоненты.
  • Нужны сложные готовые виджеты «под ключ» — таблицы с группировкой, планировщики, редакторы: их проще взять специализированными пакетами.
  • Строгий корпоративный дизайн-код с готовой реализацией.