- Learn
- Stack Essentials
- shadcn/ui
- Customization Patterns
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.cssfor 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:
- ✅ Introduction and architecture
- ✅ Installation and setup
- ✅ Using components
- ✅ Customization patterns
Continue with TypeScript to understand type safety in AI-generated code.
Mark this lesson as complete to track your progress