Intermediate15 min1 prerequisite

Customize shadcn/ui components to match your design system and extend functionality.

Customization Patterns

Since you own shadcn/ui component code, you can customize everything. Learn the patterns for modifying components to match your design system.

Modifying Component Files

Direct Editing

Open the component file and modify:

Terminal
// components/ui/button.tsx - Before
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        // ...
      },
    },
  }
)

// After - Add custom variant
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        // Add your custom variant
        success: "bg-green-500 text-white hover:bg-green-600",
        warning: "bg-yellow-500 text-black hover:bg-yellow-600",
      },
    },
  }
)

Usage:

Terminal
<Button variant="success">Save Changes</Button>
<Button variant="warning">Proceed with Caution</Button>

Adding New Sizes

Terminal
// components/ui/button.tsx
const buttonVariants = cva(
  "...",
  {
    variants: {
      // ...
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
        // Add new sizes
        xs: "h-7 rounded px-2 text-xs",
        xl: "h-14 rounded-lg px-10 text-lg",
      },
    },
  }
)

Theme Customization

Modifying CSS Variables

Edit globals.css to change the entire theme:

Terminal
@layer base {
  :root {
    /* Change primary color to blue */
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;

    /* Rounder corners */
    --radius: 0.75rem;

    /* Custom brand colors */
    --brand: 262 83% 58%;
    --brand-foreground: 0 0% 100%;
  }

  .dark {
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }
}

Adding Custom Colors

1. Define CSS variables:

Terminal
:root {
  --brand: 262 83% 58%;
  --brand-foreground: 0 0% 100%;
  --success: 142 76% 36%;
  --success-foreground: 0 0% 100%;
  --warning: 38 92% 50%;
  --warning-foreground: 0 0% 0%;
}

2. Add to Tailwind config:

Terminal
// tailwind.config.ts
theme: {
  extend: {
    colors: {
      brand: {
        DEFAULT: "hsl(var(--brand))",
        foreground: "hsl(var(--brand-foreground))",
      },
      success: {
        DEFAULT: "hsl(var(--success))",
        foreground: "hsl(var(--success-foreground))",
      },
      warning: {
        DEFAULT: "hsl(var(--warning))",
        foreground: "hsl(var(--warning-foreground))",
      },
    },
  },
},

3. Use in components:

Terminal
<Button className="bg-brand text-brand-foreground hover:bg-brand/90">
  Brand Button
</Button>

Creating Component Wrappers

Extending Components

Create wrappers that add functionality:

Terminal
// components/button-with-loading.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
import { cn } from "@/lib/utils"

interface ButtonWithLoadingProps extends ButtonProps {
  loading?: boolean
}

export function ButtonWithLoading({
  children,
  loading,
  disabled,
  className,
  ...props
}: ButtonWithLoadingProps) {
  return (
    <Button
      disabled={loading || disabled}
      className={cn(className)}
      {...props}
    >
      {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
      {children}
    </Button>
  )
}

Usage:

Terminal
<ButtonWithLoading loading={isSubmitting}>
  Submit
</ButtonWithLoading>

Specialized Components

Terminal
// components/icon-button.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { cn } from "@/lib/utils"

interface IconButtonProps extends Omit<ButtonProps, 'size'> {
  icon: React.ReactNode
  label: string
}

export function IconButton({ icon, label, className, ...props }: IconButtonProps) {
  return (
    <Button
      size="icon"
      className={cn("", className)}
      aria-label={label}
      {...props}
    >
      {icon}
    </Button>
  )
}
Terminal
// components/confirm-button.tsx
"use client"

import { useState } from "react"
import { Button, ButtonProps } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"

interface ConfirmButtonProps extends ButtonProps {
  title?: string
  description?: string
  onConfirm: () => void
}

export function ConfirmButton({
  children,
  title = "Are you sure?",
  description = "This action cannot be undone.",
  onConfirm,
  ...props
}: ConfirmButtonProps) {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)} {...props}>
        {children}
      </Button>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>{title}</AlertDialogTitle>
            <AlertDialogDescription>{description}</AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction onClick={onConfirm}>Continue</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}

Advanced Variant Patterns

Using class-variance-authority

Create your own variant-based components:

Terminal
// components/status-badge.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const statusBadgeVariants = cva(
  "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold",
  {
    variants: {
      status: {
        pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
        active: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
        inactive: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200",
        error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
      },
    },
    defaultVariants: {
      status: "pending",
    },
  }
)

interface StatusBadgeProps
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof statusBadgeVariants> {}

export function StatusBadge({ className, status, ...props }: StatusBadgeProps) {
  return (
    <span className={cn(statusBadgeVariants({ status }), className)} {...props} />
  )
}

Usage:

Terminal
<StatusBadge status="active">Active</StatusBadge>
<StatusBadge status="error">Failed</StatusBadge>

Compound Variants

Handle variant combinations:

Terminal
const buttonVariants = cva(
  "inline-flex items-center justify-center...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        outline: "border border-input bg-background",
      },
      size: {
        default: "h-10 px-4",
        sm: "h-8 px-3",
      },
    },
    compoundVariants: [
      // Special styling when both outline + small
      {
        variant: "outline",
        size: "sm",
        className: "border-2",
      },
    ],
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

Theming Multiple Brands

Multi-Theme Setup

Terminal
/* globals.css */
@layer base {
  :root {
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
  }

  .dark {
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }

  /* Brand themes */
  .theme-blue {
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 0 0% 100%;
  }

  .theme-green {
    --primary: 142 76% 36%;
    --primary-foreground: 0 0% 100%;
  }

  .theme-purple {
    --primary: 262 83% 58%;
    --primary-foreground: 0 0% 100%;
  }
}

Theme Switcher

Terminal
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"

const themes = ["default", "theme-blue", "theme-green", "theme-purple"]

export function ThemeSwitcher() {
  const [theme, setTheme] = useState("default")

  const changeTheme = (newTheme: string) => {
    document.documentElement.classList.remove(...themes)
    if (newTheme !== "default") {
      document.documentElement.classList.add(newTheme)
    }
    setTheme(newTheme)
  }

  return (
    <div className="flex gap-2">
      {themes.map((t) => (
        <Button
          key={t}
          variant={theme === t ? "default" : "outline"}
          onClick={() => changeTheme(t)}
        >
          {t}
        </Button>
      ))}
    </div>
  )
}

Customizing Specific Components

Card with Custom Styles

Terminal
// components/feature-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
import { LucideIcon } from "lucide-react"

interface FeatureCardProps {
  icon: LucideIcon
  title: string
  description: string
  className?: string
}

export function FeatureCard({
  icon: Icon,
  title,
  description,
  className
}: FeatureCardProps) {
  return (
    <Card className={cn(
      "transition-all hover:shadow-lg hover:border-primary/50",
      className
    )}>
      <CardHeader>
        <div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4">
          <Icon className="h-6 w-6 text-primary" />
        </div>
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-muted-foreground">{description}</p>
      </CardContent>
    </Card>
  )
}

Custom Input with Icons

Terminal
// components/input-with-icon.tsx
import { Input, InputProps } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import { LucideIcon } from "lucide-react"

interface InputWithIconProps extends InputProps {
  icon: LucideIcon
  iconPosition?: "left" | "right"
}

export function InputWithIcon({
  icon: Icon,
  iconPosition = "left",
  className,
  ...props
}: InputWithIconProps) {
  return (
    <div className="relative">
      <Icon className={cn(
        "absolute top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
        iconPosition === "left" ? "left-3" : "right-3"
      )} />
      <Input
        className={cn(
          iconPosition === "left" ? "pl-10" : "pr-10",
          className
        )}
        {...props}
      />
    </div>
  )
}

Usage:

Terminal
import { Search, Mail } from "lucide-react"

<InputWithIcon icon={Search} placeholder="Search..." />
<InputWithIcon icon={Mail} iconPosition="right" placeholder="Email" />

AI Tool Customization Patterns

Prompting for Custom Components

When working with AI tools:

Terminal
"Create a custom Alert component that extends shadcn/ui Alert with:
- success, warning, info variants in addition to default and destructive
- optional dismiss button
- auto-dismiss after X seconds option"
Terminal
"Modify the Button component to add:
- gradient variant with customizable colors
- pulse animation variant for CTAs
- icon-only responsive behavior (text hidden on mobile)"

Claude Code Customization

Terminal
"Update my shadcn/ui theme to use:
- Primary color: #6366f1 (indigo)
- Border radius: 0.75rem
- Add success and warning semantic colors"

Best Practices

1. Preserve Original Components

Keep original shadcn/ui files intact when possible:

Terminal
components/
├── ui/              # Original shadcn/ui
   ├── button.tsx
   └── card.tsx
└── custom/          # Your extensions
    ├── button-loading.tsx
    └── feature-card.tsx

2. Use Semantic Color Names

Terminal
/* Good - semantic */
--success: 142 76% 36%;
--warning: 38 92% 50%;

/* Avoid - specific colors */
--green-custom: 142 76% 36%;
--yellow-custom: 38 92% 50%;

3. Document Your Customizations

Terminal
/**
 * Custom Alert variants extending shadcn/ui Alert
 *
 * @variant success - For successful operations
 * @variant warning - For warning messages
 * @variant info - For informational messages
 */
const alertVariants = cva(...)

4. Type Your Extensions

Terminal
// Extend existing types properly
interface ExtendedButtonProps extends ButtonProps {
  loading?: boolean
  leftIcon?: React.ReactNode
  rightIcon?: React.ReactNode
}

Summary

  • Direct editing: Modify component files in components/ui/
  • CSS variables: Change globals.css for theme-wide changes
  • Wrappers: Create components that extend existing ones
  • CVA variants: Add custom variants with class-variance-authority
  • Multi-theme: Use CSS classes for brand themes
  • Best practices: Keep originals, use semantic names, type properly

Module Complete

You've learned shadcn/ui essentials:

  1. ✅ Introduction and architecture
  2. ✅ Installation and setup
  3. ✅ Using components
  4. ✅ Customization patterns

Continue with TypeScript to understand type safety in AI-generated code.

Mark this lesson as complete to track your progress