shadcn/ui: как форкнуть и не сломать обновления
shadcn/ui устроен иначе, чем традиционные UI-киты. Это не библиотека, которую ставишь через npm — это набор компонентов, которые генерируются прямо в твой проект. Это даёт максимальный контроль и одновременно создаёт типичный страх: «я форкнул, поправил, как теперь принимать обновления?».
Я работал с shadcn в трёх проектах, в одном из них сильно перекроил исходные компоненты. Расскажу, какой workflow у меня прижился и почему.
Что такое shadcn/ui
Когда ты делаешь npx shadcn add button, утилита идёт на сайт, скачивает button.tsx и кладёт в src/components/ui/button.tsx. Дальше код — твой. Хочешь поправить — правь. Хочешь добавить вариант — добавляй.
Технически за этим стоит Radix UI (примитивы) и Tailwind (стили). Все компоненты shadcn — это композиции Radix-примитивов и стилизация через CVA (class-variance-authority).
Главный вопрос: как принимать обновления
Команда shadcn периодически обновляет компоненты — добавляют доступность, фиксят баги, меняют структуру. Если ты не правил локальный файл, обновление прийдёт чисто:
npx shadcn diff button
npx shadcn add button --overwriteНо если ты добавил свой variant="premium" и поменял отступы, простой --overwrite сотрёт твои изменения. Здесь и начинается «как форкнуть».
Подход 1: тонкая обёртка
Я не правлю исходный button.tsx. Вместо этого создаю src/components/buttons/PrimaryButton.tsx, который импортирует базовый Button и расширяет:
import { Button, type ButtonProps } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type Props = ButtonProps & { tone?: 'brand' | 'danger' };
export function PrimaryButton({ tone = 'brand', className, ...rest }: Props) {
return (
<Button
className={cn(
tone === 'brand' && 'bg-brand text-white hover:bg-brand/90',
tone === 'danger' && 'bg-red-600 text-white hover:bg-red-700',
className,
)}
{...rest}
/>
);
}Базовый Button остаётся в components/ui/button.tsx неизменным. Обновления подтягиваются спокойно.
Подход 2: расширение через CVA
Если хочется использовать стандартный API variant="...", можно расширить buttonVariants:
// src/components/ui/button.tsx — наш форк, но с маркером
import { cva, type VariantProps } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background ...',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// === LOCAL ===
premium: 'bg-amber-500 text-black hover:bg-amber-400',
// === END LOCAL ===
},
size: { default: 'h-10 px-4 py-2', sm: 'h-9 px-3', lg: 'h-11 px-8' },
},
defaultVariants: { variant: 'default', size: 'default' },
},
);Маркеры === LOCAL === — мой приём. Когда буду брать обновление, я визуально (или скриптом) сохраняю строки между маркерами. Это не идеально, но работает.
Подход 3: registry-форк
Если правок много, можно настроить свой реестр компонентов. shadcn в 2025 году это нативно поддержал. В components.json прописываешь свой URL:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"tailwind": { "config": "", "css": "src/styles/globals.css" },
"aliases": { "components": "@/components", "utils": "@/lib/utils" },
"registries": {
"@my-team": "https://components.example.com/{name}.json"
}
}Дальше: npx shadcn add @my-team/button — берёт из вашего реестра, не из публичного. Это рабочий путь для команд, у которых много кастомизаций.
Workflow обновлений
Мой план приёма обновлений в проекте, где правки минимальны (подход 1):
- Раз в две недели запускаю
npx shadcn diff. Утилита показывает, что изменилось у меня и в апстриме. - Просматриваю diff. Там, где changes только в апстриме — применяю
npx shadcn add <component> --overwrite. - Где есть и мои правки, и апстрим — мерджу руками.
- Прогоняю Storybook и визуальные регрессии.
В проекте, где правок много (подход 2 или 3) — раз в месяц-два. И обязательно смотрю CHANGELOG апстрима: важные правки доступности (a11y) выкатывать стоит сразу.
Storybook как контракт
Без визуальной регрессии форк shadcn становится опасным. Я завожу Storybook на каждый компонент: один файл — все варианты, состояния (hover, focus, disabled), несколько размеров.
После обновления прогоняю chromatic или просто storybook test --shots и смотрю diff. Это дисциплинирует и команду — изменения в shadcn-компонентах никто не пропускает молча.
Темы и токены
shadcn использует Tailwind-переменные для тем. Я их выношу в свой theme.css:
@layer base {
:root {
--primary: 220 90% 56%;
--primary-foreground: 0 0% 100%;
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--radius: 0.5rem;
}
.dark {
--primary: 220 90% 60%;
--primary-foreground: 0 0% 100%;
--background: 222 47% 6%;
--foreground: 0 0% 98%;
}
}Главное — не править значения в каждом компоненте. Если вы меняете bg-primary на bg-blue-500 — это путь в боль. Поменяйте переменную --primary, и все компоненты подхватят сами.
Что я бы делал иначе
На первом проекте я начал агрессивно править исходные файлы. Через полгода обнаружил, что 14 компонентов из 20 — мои форки, и их обновление стало невозможным. На втором проекте взял правило: сначала тонкая обёртка, и только если она не справляется — править ui-файл. С тех пор за два года ни одного болезненного обновления.
Чек-лист, прежде чем правишь shadcn-файл
- Можно ли это решить через
classNameв месте использования? - Можно ли оборачивающим компонентом, который добавит логику?
- Можно ли через расширение
cva? - Если ответ на всё — нет, тогда правлю исходник, ставлю маркеры, фиксирую в README, какие компоненты теперь форкнуты.
Что копать дальше
shadcn хорош именно тем, что подходит к UI-кит как к коду в твоём репозитории, а не к библиотеке. Это требует дисциплины: без неё форк превращается в технический долг. С дисциплиной — это самый гибкий инструмент для построения дизайн-системы. Работа с ним больше похожа на работу с кодом, чем с зависимостью, и у этой модели свои правила.