본문으로 건너뛰기
cccskills
무료GitHub에 공개

shadcn-ui

Deep expertise on shadcn/ui component library including installation, customization, theming, and accessibility patterns. Use when building UI with shadcn/ui components, integrating Radix UI primitives, configuring Tailwind CSS v4 themes, or implementing dark mode. Covers Next.js App Router, Pages Router, and Vite setups.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(10)

  • SKILL.md14.9 KB
  • commands/shadcn-ui.md109 B
  • hooks/post-execute.cjs296 B
  • hooks/pre-execute.cjs413 B
  • references/research-requirements.md592 B
  • rules/shadcn-ui.md502 B
  • schemas/input.schema.json787 B
  • schemas/output.schema.json253 B
  • scripts/main.cjs1.3 KB
  • templates/implementation-template.md183 B

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

shadcn/ui Expert

Comprehensive guide to building UIs with shadcn/ui -- the copy-paste component library built on Radix UI primitives and Tailwind CSS. Components are not installed as a dependency; they are copied into your project for full ownership and customization.

When to Apply

Use this skill when:

  • Adding shadcn/ui components to a React or Next.js project
  • Customizing component styles, variants, or behavior
  • Setting up Tailwind CSS v4 theming with CSS variables
  • Implementing dark mode with shadcn/ui
  • Building accessible forms, dialogs, or data tables
  • Choosing between shadcn/ui components and custom implementations

Core Concepts

shadcn/ui is NOT a Component Library

shadcn/ui is a collection of reusable components that you copy into your project. Key differences from traditional libraries:

  • No npm package dependency -- components live in your codebase
  • Full ownership -- modify any component freely
  • Radix UI primitives -- accessible, unstyled headless components under the hood
  • Tailwind CSS -- all styling via utility classes and CSS variables
  • CLI-driven -- npx shadcn@latest add button copies component code

Architecture

Your Project
  components/
    ui/              <- shadcn/ui components live here
      button.tsx
      dialog.tsx
      input.tsx
      ...
  lib/
    utils.ts         <- cn() utility (clsx + tailwind-merge)

Setup

Next.js App Router (Recommended)

# Initialize shadcn/ui in existing Next.js project
npx shadcn@latest init

# This creates:
# - components.json (configuration)
# - lib/utils.ts (cn utility)
# - Tailwind CSS variable theme in globals.css

components.json Configuration:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

Vite + React

# Initialize
npx shadcn@latest init

# Vite requires path aliases in vite.config.ts:
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

Next.js Pages Router

Same as App Router but set rsc: false in components.json since Pages Router does not support React Server Components.

Adding Components

# Add a single component
npx shadcn@latest add button

# Add multiple components
npx shadcn@latest add button card input label

# Add all components
npx shadcn@latest add --all

# View available components
npx shadcn@latest add --list

The cn() Utility

Every shadcn/ui component uses cn() for conditional class merging:

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

cn() combines clsx (conditional classes) with tailwind-merge (resolves Tailwind conflicts):

<Button
  className={cn(
    'bg-primary text-white',
    isDisabled && 'opacity-50 cursor-not-allowed',
    size === 'lg' && 'px-8 py-4 text-lg'
  )}
>
  Submit
</Button>

Theming with CSS Variables

Tailwind CSS v4 Theme Setup

shadcn/ui uses CSS custom properties for theming, enabling runtime theme switching:

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 240 10% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --primary-foreground: 0 0% 98%;
    --secondary: 240 4.8% 95.9%;
    --secondary-foreground: 240 5.9% 10%;
    --muted: 240 4.8% 95.9%;
    --muted-foreground: 240 3.8% 46.1%;
    --accent: 240 4.8% 95.9%;
    --accent-foreground: 240 5.9% 10%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 5.9% 90%;
    --input: 240 5.9% 90%;
    --ring: 240 5.9% 10%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --card: 240 10% 3.9%;
    --card-foreground: 0 0% 98%;
    --popover: 240 10% 3.9%;
    --popover-foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    --secondary: 240 3.7% 15.9%;
    --secondary-foreground: 0 0% 98%;
    --muted: 240 3.7% 15.9%;
    --muted-foreground: 240 5% 64.9%;
    --accent: 240 3.7% 15.9%;
    --accent-foreground: 0 0% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 3.7% 15.9%;
    --input: 240 3.7% 15.9%;
    --ring: 240 4.9% 83.9%;
  }
}

Dark Mode Implementation

Use next-themes for Next.js dark mode:

// app/providers.tsx
'use client';

import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Toggle component:

'use client';

import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';

export function ThemeToggle() {
  const { setTheme, theme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

Common Component Patterns

Forms with React Hook Form + Zod

'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';

const formSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export function LoginForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { email: '', password: '' },
  });

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="name@example.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign In</Button>
      </form>
    </Form>
  );
}

Data Tables with TanStack Table

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map(headerGroup => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <TableHead key={header.id}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows.map(row => (
            <TableRow key={row.id}>
              {row.getVisibleCells().map(cell => (
                <TableCell key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}

Responsive Dialog / Drawer Pattern

Use Dialog on desktop and Drawer on mobile:

'use client';

import { useMediaQuery } from '@/hooks/use-media-query';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { Button } from '@/components/ui/button';

export function ResponsiveModal({ children }: { children: React.ReactNode }) {
  const isDesktop = useMediaQuery('(min-width: 768px)');

  if (isDesktop) {
    return (
      <Dialog>
        <DialogTrigger asChild>
          <Button>Open</Button>
        </DialogTrigger>
        <DialogContent>{children}</DialogContent>
      </Dialog>
    );
  }

  return (
    <Drawer>
      <DrawerTrigger asChild>
        <Button>Open</Button>
      </DrawerTrigger>
      <DrawerContent>{children}</DrawerContent>
    </Drawer>
  );
}

Accessibility Patterns

shadcn/ui components are built on Radix UI, which provides:

  • Full keyboard navigation (Tab, Arrow keys, Enter, Escape)
  • ARIA attributes (roles, states, properties)
  • Focus management (trapping, restoration)
  • Screen reader announcements

Key Accessibility Features by Component

ComponentKeyboardARIAFocus Trap
ButtonEnter/Space to activaterole="button"No
DialogEscape to closerole="dialog", aria-modalYes
Dropdown MenuArrow keys to navigaterole="menu", role="menuitem"Yes
SelectArrow keys, type-aheadrole="listbox", role="option"Yes
TabsArrow keys between tabsrole="tablist", role="tab"No
ToastAuto-announcerole="status", aria-liveNo
TooltipFocus/hover to showrole="tooltip"No

Custom Accessibility Enhancements

// Always provide labels for interactive elements
<Button aria-label="Close dialog">
  <X className="h-4 w-4" />
</Button>

// Use sr-only for visual-only content
<span className="sr-only">Loading...</span>

// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>

Anti-Patterns

  • Do NOT install shadcn/ui as an npm package -- use the CLI to copy components
  • Do NOT modify Radix primitives directly -- extend via the shadcn wrapper component
  • Do NOT use hardcoded colors -- always use CSS variable theme tokens
  • Do NOT skip the cn() utility -- it prevents Tailwind class conflicts
  • Do NOT forget suppressHydrationWarning on <html> when using next-themes
  • Do NOT nest interactive elements (button inside button, link inside button)

Iron Laws

  1. NEVER install shadcn/ui as a package dependency — components must be copied into the project for full ownership
  2. ALWAYS use the cn() utility for conditional class names to prevent Tailwind class conflicts
  3. NEVER hardcode colors — always use CSS variable theme tokens for theming consistency
  4. ALWAYS use Radix UI primitives through the shadcn/ui abstraction, not directly
  5. NEVER nest interactive elements (button inside button, link inside button) — violates accessibility standards

Anti-Patterns

Anti-PatternWhy It FailsCorrect Approach
Installing as a packageComponent source is locked; no customization possibleUse npx shadcn@latest add to copy components into your project
Hardcoding color valuesTheme switching breaks; dark mode failsUse CSS variable tokens (bg-background, text-foreground, etc.)
Skipping cn() utilityTailwind class conflicts produce unpredictable stylesAlways merge classes with cn() from @/lib/utils
Direct Radix UI primitive useMissing shadcn styling and accessibility wiringUse shadcn components that wrap Radix primitives with correct classes
Missing suppressHydrationWarningHydration mismatch errors with next-themes dark modeAdd suppressHydrationWarning to <html> when using next-themes

Memory Protocol (MANDATORY)

Before starting: Read .claude/context/memory/learnings.md

After completing:

  • New pattern -> .claude/context/memory/learnings.md
  • Issue found -> .claude/context/memory/issues.md
  • Decision made -> .claude/context/memory/decisions.md

ASSUME INTERRUPTION: If it's not in memory, it didn't happen.

References

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Ensure accessibility in UI components including semantic HTML, ARIA attributes, keyboard navigation, and WCAG 2.2 AA compliance.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

Use when you want to improve response quality through meta-cognitive reasoning. Applies 15+ reasoning methods to reconsider and refine initial outputs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

N-round opposing-stance debates for trade-off analysis. Assigns pro/con roles to agents, runs structured debate rounds with quality scoring, and produces a moderator synthesis with confidence-rated recommendation. Generalizable to architecture, technology, security, and design decisions.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

Force adversarial code review stance that eliminates confirmation bias — reviewer must find issues or re-analyze

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

Creates specialized AI agents on-demand when no existing agent matches a request. Use when the Router cannot find a suitable agent for a task. Enables self-evolution by generating persistent agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

LLM-as-judge evaluation framework with 5-dimension rubric (accuracy, groundedness, coherence, completeness, helpfulness) for scoring AI-generated content quality with weighted composite scores and evidence citations

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

oimiragieo/agent-studio422026년 7월 14일 업데이트

oimiragieo의 스킬 모두 보기

이 스킬의 문제 신고