Building Beautiful UIs with Tailwind CSS and shadcn/ui
Discover how to create stunning, accessible user interfaces using Tailwind CSS and the shadcn/ui component library in your Next.js applications.

Building Beautiful UIs with Tailwind CSS and shadcn/ui
Creating beautiful, consistent, and accessible user interfaces has never been easier thanks to the powerful combination of Tailwind CSS and shadcn/ui. In this guide, we'll explore how to leverage these tools to build stunning React applications.
Why Tailwind CSS + shadcn/ui?
This combination offers several advantages:
- 🎨 Design Consistency: Pre-built components with consistent styling
- ⚡ Developer Productivity: Copy-paste components that just work
- ♿ Accessibility: Built-in accessibility features
- 🎯 Customization: Easy to customize and extend
- 📱 Responsive: Mobile-first responsive design
Setting Up Your Environment
1. Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configure your tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
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))',
},
// ... more colors
},
},
},
plugins: [],
};
2. Initialize shadcn/ui
npx shadcn@latest init
This will set up the necessary configuration files and install core dependencies.
Essential Components
Buttons
The Button component is the foundation of most interactions:
import { Button } from "@/components/ui/button"
export default function ButtonDemo() {
return (
<div className="space-x-2">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
)
}
Cards
Perfect for organizing content:
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export default function CardDemo() {
return (
<Card className="w-96">
<CardHeader>
<CardTitle>Feature Highlight</CardTitle>
<CardDescription>
Discover what makes this feature special
</CardDescription>
</CardHeader>
<CardContent>
<p>Your content goes here...</p>
</CardContent>
<CardFooter>
<Button className="w-full">Get Started</Button>
</CardFooter>
</Card>
)
}
Forms
Building accessible forms is straightforward:
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
export default function ContactForm() {
const form = useForm()
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter your username" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Advanced Patterns
Theme Switching
Implement dark/light mode easily:
"use client"
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"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
Custom Color Palette
Extend your design system:
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode colors */
}
Responsive Components
Build mobile-first designs:
export default function ResponsiveGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<Card key={item.id} className="h-full">
<CardHeader>
<CardTitle className="text-sm md:text-base lg:text-lg">
{item.title}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs md:text-sm text-muted-foreground">
{item.description}
</p>
</CardContent>
</Card>
))}
</div>
)
}
Component Composition
Building Complex Layouts
Combine components to create sophisticated interfaces:
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
export default function ProductCard({ product }) {
return (
<Card className="overflow-hidden">
<div className="aspect-video bg-muted" />
<CardHeader>
<div className="flex items-start justify-between">
<CardTitle className="line-clamp-2">{product.name}</CardTitle>
<Badge variant={product.inStock ? "default" : "secondary"}>
{product.inStock ? "In Stock" : "Out of Stock"}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground line-clamp-3">
{product.description}
</p>
<Separator />
<div className="flex items-center justify-between">
<span className="text-2xl font-bold">${product.price}</span>
<Button disabled={!product.inStock}>
Add to Cart
</Button>
</div>
</CardContent>
</Card>
)
}
Performance Optimization
Bundle Size Optimization
Import only what you need:
// ❌ Don't import the entire library
import * as lucide from 'lucide-react';
// ✅ Import specific icons
import { ChevronDown, User, Settings } from 'lucide-react';
CSS-in-JS Alternative
Use Tailwind's utility classes for better performance:
// ❌ Runtime CSS-in-JS
const styles = {
button: {
backgroundColor: '#3b82f6',
color: 'white',
padding: '0.5rem 1rem',
borderRadius: '0.25rem',
}
}
// ✅ Tailwind utilities (compile-time)
<Button className="bg-blue-500 text-white px-4 py-2 rounded">
Click me
</Button>
Accessibility Best Practices
Keyboard Navigation
Ensure all interactive elements are keyboard accessible:
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function AccessibleDropdown() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" aria-label="Open menu">
Menu
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onSelect={() => console.log('Profile')}>
Profile
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => console.log('Settings')}>
Settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
Screen Reader Support
Use semantic HTML and ARIA attributes:
export default function StatusCard({ status, message }) {
return (
<Card role="alert" aria-live="polite">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<StatusIcon status={status} aria-hidden="true" />
<span className="sr-only">Status: </span>
{status}
</CardTitle>
</CardHeader>
<CardContent>
<p>{message}</p>
</CardContent>
</Card>
)
}
Testing Your Components
Component Testing with Jest
import { render, screen } from '@testing-library/react'
import { Button } from '@/components/ui/button'
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button')).toHaveTextContent('Click me')
})
it('applies variant styles', () => {
render(<Button variant="destructive">Delete</Button>)
expect(screen.getByRole('button')).toHaveClass('bg-destructive')
})
})
Conclusion
The combination of Tailwind CSS and shadcn/ui provides a powerful foundation for building beautiful, accessible, and maintainable user interfaces. By following the patterns and best practices outlined in this guide, you'll be able to create stunning applications that delight your users.
Start building your design system today and experience the joy of consistent, beautiful UI development!
Want to explore more components? Check out the shadcn/ui documentation and discover the full range of available components.