Реестр вместо пакета
Установка обычной библиотеки выглядит так: 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 и примитивы — то есть всё, что было в первой половине курса.
Сравнение подходов
| Библиотека (MUI, Chakra, Ant) | shadcn/ui | |
|---|---|---|
| Где живёт код | node_modules | ваш репозиторий |
| Кастомизация | через API темы и переопределения | правкой исходника |
| Обновления | npm update | повторное добавление, ручной diff |
| Риск сломать дизайн | при мажорном обновлении | только когда вы сами обновите |
| Скорость старта | очень высокая | высокая |
| Единство в команде | обеспечено библиотекой | обеспечивается дисциплиной |
| Размер бандла | зависит от tree-shaking | только добавленные компоненты |
Доступность из коробки
Самая недооценённая часть. Примитив берёт на себя то, что почти никто не реализует правильно вручную:
- ловушка фокуса в диалоге и возврат фокуса при закрытии;
- навигация стрелками в меню, вкладках и списках;
- закрытие по Esc и клику вне;
- корректные
aria-*и связиaria-labelledby; - отрисовка через портал, чтобы всплывающее не обрезалось родителем;
- блокировка прокрутки страницы под модальным окном.
Выбор примитивов
Современный 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: смысла мало, компоненты оформлены утилитами.
- Команда без фронтенд-ресурса: некому вести компоненты.
- Нужны сложные готовые виджеты «под ключ» — таблицы с группировкой, планировщики, редакторы: их проще взять специализированными пакетами.
- Строгий корпоративный дизайн-код с готовой реализацией.