К содержанию

shadcn/ui: компоненты в вашем проекте

Добавляем исходники кнопки, карточки и диалога через CLI.

На этой странице

Для Next.js 16.3.8 (App Router) и React 19.3.0, Tailwind CSS 4.3.3, CLI shadcn 4.21.1. Обновлено .

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

Инициализация

CLI – инструмент, которым управляют командами в терминале. В нашем случае он настраивает проект и добавляет файлы компонентов.

Команды выполняются в папке созданного Next.js проекта с Tailwind. Сохраните свои изменения в Git перед настройкой, чтобы было удобно посмотреть разницу.

Терминал
bunx shadcn@latest init --base radix
bunx shadcn@latest add button input label card

CLI предложит параметры оформления и путей. Для примеров этой статьи выберите Radix – библиотеку, на которой основано поведение этих компонентов. Оставьте алиас компонентов @/components, если не выбирали другую структуру. Для компонентов здесь используется путь @/components/ui.

components.json хранит настройки CLI: куда добавлять компоненты, где находится CSS, какие используются алиасы. Это конфигурация инструмента, а не компонент React.

После установки посмотрите Git diff. CLI может добавить зависимости, функцию cn, токены и исходники компонентов. Проверьте, что app/layout.tsx подключает CSS и что в globals.css нет двух разных наборов переменных, которые задают одни и те же цвета. Цвета из предыдущего упражнения можно перенести в существующие роли shadcn/ui.

Кнопка вместо собственного оформления

app/ticket-counter.tsx
"use client";
 
import { useState } from "react";
import { Button } from "@/components/ui/button";
 
export function TicketCounter() {
  const [count, setCount] = useState(1);
 
  return (
    <div className="flex flex-wrap items-center gap-3">
      <p>Билетов: {count}</p>
      <Button
        type="button"
        variant="outline"
        disabled={count === 1}
        onClick={() => setCount((current) => Math.max(1, current - 1))}
      >
        Убрать
      </Button>
      <Button type="button" onClick={() => setCount((current) => current + 1)}>
        Добавить
      </Button>
    </div>
  );
}

variant – параметр компонента: он выбирает предусмотренный вариант оформления. disabled отключает кнопку. Обработчик добавления остался обычной функцией React: shadcn/ui оформляет интерфейс, а состояние принадлежит вашему компоненту.

type="button" указываем явно. Если позже перенести эту кнопку внутрь формы, она не начнет случайно отправлять её.

Карточка с теми же параметрами

Замените учебную карточку, сохранив прежние props. Страница и фильтр продолжат использовать её без изменения данных:

app/event-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 
type EventCardProps = { name: string; price: number };
 
export function EventCard({ name, price }: EventCardProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p>{price === 0 ? "Бесплатно" : `${price} ₽`}</p>
      </CardContent>
    </Card>
  );
}

У Card уже есть роли фона, текста и границы. Сравните обе темы, затем измените общий токен: карточка должна получить новый цвет без отдельной правки компонента.

Где менять внешний вид

ИзменениеПодходящее место
Другой основной акцент во всём проектеТокены в globals.css
Расположение элементов одной карточкиclassName карточки
Ещё один повторяемый вариант кнопкиВарианты в исходнике Button
Логика фильтра или формыКомпонент приложения, использующий UI

Не перекрашивайте каждую кнопку отдельно, если нужно изменить общий цвет интерфейса. И не добавляйте логику афиши внутрь универсального Button: он может использоваться в других местах.

Диалог – больше, чем прямоугольник поверх страницы

Для модального окна нужны перенос в подходящее место DOM, затемнение, управление фокусом, Escape и возврат фокуса к кнопке. Добавьте готовый компонент:

Терминал
bunx shadcn@latest add dialog
app/event-details.tsx
"use client";
 
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
 
export function EventDetails() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button type="button" variant="outline">
          Подробнее о кинопоказе
        </Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Кинопоказ</DialogTitle>
          <DialogDescription>
            Обсуждение фильма после просмотра. Начало в 19:00.
          </DialogDescription>
        </DialogHeader>
        <p>Приходите за десять минут до начала.</p>
      </DialogContent>
    </Dialog>
  );
}

Пример рассчитан на компоненты shadcn/ui с Radix, которые поддерживают asChild. Если выбрали другую основу компонентов, проверьте в её документации, как подключается кнопка открытия диалога. asChild позволяет использовать кнопку как триггер, не создавая кнопку внутри другой кнопки.

Импортируйте EventDetails в страницу и добавьте <EventDetails />. Проверьте Tab, Escape, возврат фокуса и узкий экран. После добавления диалога всё равно проверьте, что его содержимое помещается в окне и им удобно пользоваться.

Попробуйте: оформите счетчик, фильтр и состояния афиши. Сравните обе темы. Формы с полями и сообщениями разберем в Next-04, когда будет понятна их логика.

Документация: установка Next.js, Button, Dialog, темы.