shadcn/ui + Tailwind CSS Skill
Modern component library and styling approach using shadcn/ui components with Tailwind CSS utility classes.
Core Philosophy
shadcn/ui Approach:
- Copy components into your project (not npm packages)
- Full ownership and customization
- Built on Radix UI primitives
- Accessible by default
- Composable components
Tailwind Philosophy:
- Utility-first CSS
- Design in markup
- Consistent design system
- Responsive by default
- Performance optimized
Installation & Setup
Initial Setup
# Install dependencies
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Install shadcn/ui
npx shadcn-ui@latest init
Tailwind Configuration
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
Installing Components
# Install individual components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add form
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npx shadcn-ui@latest add select
npx shadcn-ui@latest add table
npx shadcn-ui@latest add toast
# Common combinations
npx shadcn-ui@latest add button card dialog form input label toast
Common Component Patterns
Form with Validation
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,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Data Table
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function DataTable({ data }: { data: Array<{ id: string; name: string; email: string }> }) {
return (
<Table>
<TableCaption>A list of your recent users.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.email}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
Dialog with Form
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function CreateUserDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Create User</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create user</DialogTitle>
<DialogDescription>
Add a new user to your workspace.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input id="email" type="email" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Create user</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Toast Notifications
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
export function ToastExample() {
const { toast } = useToast()
return (
<Button
=> {
toast({
title: "Success!",
description: "Your changes have been saved.",
})
}}
>
Show Toast
</Button>
)
}
// Error toast
toast({
variant: "destructive",
title: "Uh oh! Something went wrong.",
description: "There was a problem with your request.",
})
Tailwind Best Practices
Layout Patterns
// Container with max width
<div className="container mx-auto px-4">
{/* content */}
</div>
// Card layout
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{items.map(item => (
<Card key={item.id} className="p-6">
{/* card content */}
</Card>
))}
</div>
// Centered content
<div className="flex min-h-screen items-center justify-center">
{/* centered content */}
</div>
// Sticky header
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
{/* header content */}
</header>
Responsive Design
// Mobile-first responsive classes
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{/* responsive grid */}
</div>
// Responsive text
<h1 className="text-2xl md:text-4xl lg:text-6xl font-bold">
Responsive Heading
</h1>
// Responsive spacing
<div className="p-4 md:p-6 lg:p-8">
{/* responsive padding */}
</div>
// Show/hide on breakpoints
<div className="hidden md:block">Desktop only</div>
<div className="block md:hidden">Mobile only</div>
State Variants
// Hover, focus, active states
<button className="bg-primary hover:bg-primary/90 focus:ring-2 focus:ring-primary focus:ring-offset-2 active:scale-95 transition-all">
Click me
</button>
// Disabled state
<button className="disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Disabled
</button>
// Group hover
<div className="group">
<img className="group-hover:scale-110 transition-transform" />
<p className="group-hover:text-primary">Hover the parent</p>
</div>
Dark Mode
// Install next-themes
npm install next-themes
// app/providers.tsx
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// Theme toggle component
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
=> setTheme(theme === "light" ? "dark" : "light")}
>
<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>
)
}
Accessibility
// Proper ARIA labels
<Button aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// Screen reader only text
<span className="sr-only">Loading...</span>
// Focus visible
<button className="focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2">
Click me
</button>
Performance Tips
// Avoid className conflicts - use cn() helper
import { cn } from "@/lib/utils"
<Button className={cn("custom-class", someCondition && "conditional-class")}>
Click me
</Button>
// Lazy load heavy components
const HeavyChart = lazy(() => import("@/components/heavy-chart"))
Common Gotchas
- CSS Variable Format: Use
hsl(var(--primary))notvar(--primary) - Dark Mode: Must add
darkMode: ["class"]to Tailwind config - Form Components: Always use with React Hook Form
- Component Updates: Run
npx shadcn-ui@latest diff - TypeScript: Ensure
components.jsonhas correct paths - Animation Plugin: Install
tailwindcss-animate