Claude
Skills
Sign in
Back

shadcn-ui

Included with Lifetime
$97 forever

shadcn/ui component patterns with Radix primitives and Tailwind styling. Use when building UI components, using CVA variants, implementing compound components, or styling with data-slot attributes. Triggers on shadcn, cva, cn(), data-slot, Radix, Button, Card, Dialog, VariantProps.

Design

What this skill does


# shadcn/ui Component Development

## Contents

- [CLI Commands](#cli-commands) - Installing and adding components
- [Quick Reference](#quick-reference) - cn(), basic CVA pattern
- [Component Anatomy](#component-anatomy) - Props typing, asChild, data-slot
- [Component Patterns](#component-patterns) - Compound components
- [Styling Techniques](#styling-techniques) - CVA variants, modern CSS selectors, accessibility states
- [Decision Tables](#decision-tables) - When to use CVA, compound components, asChild, Context
- [Common Patterns](#common-patterns) - Form elements, dialogs, sidebars
- [Reference Files](#reference-files) - Full implementations and advanced patterns

## CLI Commands

### Initialize shadcn/ui

```bash
npx shadcn@latest init
```

This creates a `components.json` configuration file and sets up:
- Tailwind CSS configuration
- CSS variables for theming
- cn() utility function
- Required dependencies

### Add Components

```bash
# Add a single component
npx shadcn@latest add button

# Add multiple components
npx shadcn@latest add button card dialog

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

**Important:** The package name changed in 2024:
- Old (deprecated): `npx shadcn-ui@latest add`
- Current: `npx shadcn@latest add`

### Common Options

- `-y, --yes` - Skip confirmation prompt
- `-o, --overwrite` - Overwrite existing files
- `-c, --cwd <cwd>` - Set working directory
- `--src-dir` - Use src directory structure

### Gates (CLI and file changes)

Run these **in order** before `init` / `add` (skip only when you are not running the CLI—e.g. copying snippets from this skill):

1. **Working directory:** **Pass:** `pwd` and `package.json` at that path identify the app root that should receive `components/` and `components.json` (use `-c <cwd>` if the shell is elsewhere).
2. **After `init`:** **Pass:** `components.json` exists at that app root (or the documented path for your monorepo layout).
3. **Before `--overwrite`:** **Pass:** you can list which tracked files will be replaced, or version control shows the change is intentional and recoverable.

## Quick Reference

### cn() Utility

```tsx
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

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

### Basic CVA Pattern

```tsx
import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
  "base-classes-applied-to-all-variants",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        outline: "border bg-background",
      },
      size: {
        sm: "h-8 px-3",
        lg: "h-10 px-6",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "sm",
    },
  }
)

function Button({
  variant,
  size,
  className,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

export { Button, buttonVariants }
```

## Component Anatomy

### Props Typing Patterns

```tsx
// HTML elements
function Component({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("base-classes", className)} {...props} />
}

// Radix primitives
function Component({ className, ...props }: React.ComponentProps<typeof RadixPrimitive.Root>) {
  return <RadixPrimitive.Root className={cn("base-classes", className)} {...props} />
}

// With CVA variants
function Component({
  variant, size, className, ...props
}: React.ComponentProps<"button"> & VariantProps<typeof variants>) {
  return <button className={cn(variants({ variant, size }), className)} {...props} />
}
```

### asChild Pattern

Enables polymorphic rendering via `@radix-ui/react-slot`:

```tsx
import { Slot } from "@radix-ui/react-slot"

function Button({
  asChild = false,
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "button"
  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}
```

**Usage:**
```tsx
<Button>Click me</Button>                           // Renders <button>
<Button asChild><a href="/home">Home</a></Button>   // Renders <a> with button styling
<Button asChild><Link href="/dash">Dash</Link></Button>  // Works with Next.js Link
```

### data-slot Attributes

Every component includes `data-slot` for CSS targeting:

```tsx
function Card({ ...props }) { return <div data-slot="card" {...props} /> }
function CardHeader({ ...props }) { return <div data-slot="card-header" {...props} /> }
```

**CSS/Tailwind targeting:**
```css
[data-slot="button"] { /* styles */ }
[data-slot="card"] [data-slot="button"] { /* nested targeting */ }
```

```tsx
<div className="[&_[data-slot=button]]:shadow-lg">
  <Button>Automatically styled</Button>
</div>
```

**Conditional layouts with has():**
```tsx
<div
  data-slot="card-header"
  className={cn(
    "grid gap-2",
    "has-data-[slot=card-action]:grid-cols-[1fr_auto]"
  )}
/>
```

## Component Patterns

### Compound Components

```tsx
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }

function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-header" className={cn("grid gap-2 px-6", className)} {...props} />
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />
}
```

## Styling Techniques

### CVA Variants

**Multiple dimensions:**
```tsx
const buttonVariants = cva("base-classes", {
  variants: {
    variant: {
      default: "bg-primary text-primary-foreground",
      destructive: "bg-destructive text-white",
      outline: "border bg-background",
      ghost: "hover:bg-accent",
      link: "text-primary underline-offset-4 hover:underline",
    },
    size: {
      default: "h-9 px-4 py-2",
      sm: "h-8 px-3",
      lg: "h-10 px-6",
      icon: "size-9",
    },
  },
  defaultVariants: { variant: "default", size: "default" },
})
```

**Compound variants:**
```tsx
compoundVariants: [
  { variant: "outline", size: "lg", class: "border-2" },
]
```

**Type extraction:**
```tsx
type ButtonVariants = VariantProps<typeof buttonVariants>
// Result: { variant?: "default" | "outline" | ..., size?: "sm" | "lg" | ... }
```

### Modern CSS Selectors in Tailwind

**has() selector:**
```tsx
<button className="px-4 has-[>svg]:px-3">  // Adjusts padding when contains icon
<div className="has-data-[slot=action]:grid-cols-[1fr_auto]">  // Conditional layout
```

**Group/peer selectors:**
```tsx
<div className="group" data-state="collapsed">
  <div className="group-data-[state=collapsed]:hidden">Hidden when collapsed</div>
</div>

<button className="peer/menu" data-active="true">Menu</button>
<div className="peer-data-[active=true]/menu:text-accent">Styled when sibling active</div>
```

**Container queries:**
```tsx
<div className="@container/card">
  <div className="@md:flex-row">Responds to container width</div>
</div>
```

### Accessibility States

```tsx
className={cn(
  // Focus
  "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
  // Invalid
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
  // Disabled
  "disabled:pointer-events-none disabled:opacity-50",
)}

<span className="sr-only">Close</span>  // Screen reader only
```

### Dark Mode

Semantic tokens adapt automatically:
```tsx
className=

Related in Design