Hello! Ready to master shadcn/ui? Perfect! I'll be sharing some essential practices that will help you build better, more maintainable UIs with this amazing component library.
These practices come from real-world experience and will help you avoid common pitfalls while maximizing the potential of shadcn/ui.
Component Composition
Start with Base Components
Always begin with shadcn/ui base components and compose them thoughtfully. Don't rush into creating custom variants immediately.
// ✅ Good: Start with base component
import { Button } from '@/components/ui/button';
export function ActionButton({ children, ...props }) {
return (
<Button variant="default" size="sm" {...props}>
{children}
</Button>
);
}
Create Semantic Components
Build semantic components that represent your application's domain, not just visual variants.
import { Button } from '@/components/ui/button';
import { LogIn } from 'lucide-react';
export function LoginButton({ isLoading, ...props }) {
return (
<Button disabled={isLoading} className="w-full" {...props}>
<LogIn className="mr-2 h-4 w-4" />
{isLoading ? 'Signing in...' : 'Sign In'}
</Button>
);
}
Styling Strategies
Leverage CSS Variables
Use shadcn/ui's CSS custom properties system for consistent theming across your application.
:root {
--radius: 0.5rem;
/* Custom semantic colors */
--success: 142 76% 36%;
--warning: 38 92% 50%;
--error: 0 84% 60%;
}
Extend with Tailwind Variants
Create reusable variants using Tailwind's flexible class system, but keep it organized.
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
const statusVariants = {
success: 'bg-green-600 hover:bg-green-700 text-white',
warning: 'bg-yellow-600 hover:bg-yellow-700 text-white',
error: 'bg-red-600 hover:bg-red-700 text-white',
};
export function StatusButton({ status, className, ...props }) {
return (
<Button className={cn(statusVariants[status], className)} {...props} />
);
}
Use Compound Components
For complex UI patterns, create compound components that work together seamlessly.
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
export function ProductCard({ children }) {
return <Card className="overflow-hidden">{children}</Card>;
}
ProductCard.Header = function ProductCardHeader({ title, category }) {
return (
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{title}</CardTitle>
<Badge variant="secondary">{category}</Badge>
</div>
</CardHeader>
);
};
ProductCard.Content = CardContent;
Form Best Practices
Always Use Form Components Together
shadcn/ui's form components work best when used as a cohesive system.
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<form>
<label>Username</label>
<Input name="username" />
<span className="text-red-500">{error}</span>
</form>
Implement Consistent Validation
Use a consistent validation strategy across all forms in your application.
import { z } from 'zod';
// Define reusable schemas
export const userSchema = z
.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
Performance Tips
Import Only What You Need
Don't import the entire components directory. Import specific components to keep your bundle size optimal.
// ✅ Good
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
// ❌ Avoid
import * as UI from '@/components/ui';
Use React.memo for Heavy Components
For components that render frequently or have expensive calculations, use React.memo.
import { memo } from 'react';
import { Card, CardContent } from '@/components/ui/card';
export const DataCard = memo(function DataCard({ data }) {
const processedData = useMemo(() => expensiveDataProcessing(data), [data]);
return (
<Card>
<CardContent>
{processedData.map((item) => (
<div key={item.id}>{item.value}</div>
))}
</CardContent>
</Card>
);
});
Organization Tips
Create a Design System
Establish component variants and patterns early in your project.
// lib/component-variants.ts
export const buttonVariants = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
success: 'bg-green-600 text-white hover:bg-green-700',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
export const cardVariants = {
elevated: 'shadow-lg border-0',
flat: 'shadow-none border',
interactive: 'hover:shadow-md transition-shadow cursor-pointer',
};
Document Your Components
Add proper TypeScript types and JSDoc comments for better developer experience.
interface ProductCardProps {
/** Product data to display */
product: Product;
/** Callback when product is selected */
onSelect?: (product: Product) => void;
/** Show compact version */
compact?: boolean;
/** Additional CSS classes */
className?: string;
}
/**
* Displays product information in a card format
* Supports both compact and full layouts
*/
export function ProductCard({
product,
onSelect,
compact = false,
className,
}: ProductCardProps) {
// Component implementation
}
These practices will help you build more maintainable and scalable applications with shadcn/ui. Remember, consistency is key – establish patterns early and stick to them throughout your project!
The beauty of shadcn/ui lies in its flexibility while maintaining design consistency. Use these practices as a foundation, but don't be afraid to adapt them to your specific project needs.