lenec ru

← все посты

shadcn/ui: как форкнуть и не сломать обновления

11K

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):

  1. Раз в две недели запускаю npx shadcn diff. Утилита показывает, что изменилось у меня и в апстриме.
  2. Просматриваю diff. Там, где changes только в апстриме — применяю npx shadcn add <component> --overwrite.
  3. Где есть и мои правки, и апстрим — мерджу руками.
  4. Прогоняю 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-кит как к коду в твоём репозитории, а не к библиотеке. Это требует дисциплины: без неё форк превращается в технический долг. С дисциплиной — это самый гибкий инструмент для построения дизайн-системы. Работа с ним больше похожа на работу с кодом, чем с зависимостью, и у этой модели свои правила.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.