Tailwind CSS enables rapid UI development. Here's how to build maintainable, reusable components.
Design Tokens in Tailwind#
1// tailwind.config.js
2module.exports = {
3 theme: {
4 extend: {
5 colors: {
6 brand: {
7 50: '#eff6ff',
8 100: '#dbeafe',
9 500: '#3b82f6',
10 600: '#2563eb',
11 700: '#1d4ed8',
12 },
13 success: {
14 light: '#d1fae5',
15 DEFAULT: '#10b981',
16 dark: '#059669',
17 },
18 error: {
19 light: '#fee2e2',
20 DEFAULT: '#ef4444',
21 dark: '#dc2626',
22 },
23 },
24 spacing: {
25 '4.5': '1.125rem',
26 '18': '4.5rem',
27 },
28 fontSize: {
29 'xxs': ['0.625rem', { lineHeight: '0.75rem' }],
30 },
31 borderRadius: {
32 '4xl': '2rem',
33 },
34 boxShadow: {
35 'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07)',
36 'glow': '0 0 15px rgba(59, 130, 246, 0.5)',
37 },
38 },
39 },
40};Button Component#
1// components/Button.tsx
2import { cva, type VariantProps } from 'class-variance-authority';
3import { cn } from '@/lib/utils';
4
5const buttonVariants = cva(
6 // Base styles
7 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
8 {
9 variants: {
10 variant: {
11 primary: 'bg-brand-600 text-white hover:bg-brand-700 focus-visible:ring-brand-500',
12 secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
13 outline: 'border border-gray-300 bg-transparent hover:bg-gray-50 focus-visible:ring-gray-500',
14 ghost: 'hover:bg-gray-100 hover:text-gray-900 focus-visible:ring-gray-500',
15 destructive: 'bg-error text-white hover:bg-error-dark focus-visible:ring-error',
16 link: 'text-brand-600 underline-offset-4 hover:underline',
17 },
18 size: {
19 sm: 'h-8 px-3 text-sm',
20 md: 'h-10 px-4 text-sm',
21 lg: 'h-12 px-6 text-base',
22 icon: 'h-10 w-10',
23 },
24 },
25 defaultVariants: {
26 variant: 'primary',
27 size: 'md',
28 },
29 }
30);
31
32interface ButtonProps
33 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
34 VariantProps<typeof buttonVariants> {
35 isLoading?: boolean;
36}
37
38export function Button({
39 className,
40 variant,
41 size,
42 isLoading,
43 children,
44 disabled,
45 ...props
46}: ButtonProps) {
47 return (
48 <button
49 className={cn(buttonVariants({ variant, size, className }))}
50 disabled={disabled || isLoading}
51 {...props}
52 >
53 {isLoading && (
54 <svg
55 className="mr-2 h-4 w-4 animate-spin"
56 viewBox="0 0 24 24"
57 >
58 <circle
59 className="opacity-25"
60 cx="12"
61 cy="12"
62 r="10"
63 stroke="currentColor"
64 strokeWidth="4"
65 fill="none"
66 />
67 <path
68 className="opacity-75"
69 fill="currentColor"
70 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
71 />
72 </svg>
73 )}
74 {children}
75 </button>
76 );
77}
78
79// Usage
80<Button variant="primary" size="lg">Click me</Button>
81<Button variant="outline" isLoading>Loading...</Button>
82<Button variant="destructive" size="sm">Delete</Button>Card Component#
1// components/Card.tsx
2import { cn } from '@/lib/utils';
3
4interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
5
6export function Card({ className, ...props }: CardProps) {
7 return (
8 <div
9 className={cn(
10 'rounded-lg border border-gray-200 bg-white shadow-soft',
11 className
12 )}
13 {...props}
14 />
15 );
16}
17
18export function CardHeader({ className, ...props }: CardProps) {
19 return (
20 <div
21 className={cn('flex flex-col space-y-1.5 p-6', className)}
22 {...props}
23 />
24 );
25}
26
27export function CardTitle({
28 className,
29 ...props
30}: React.HTMLAttributes<HTMLHeadingElement>) {
31 return (
32 <h3
33 className={cn('text-lg font-semibold leading-none tracking-tight', className)}
34 {...props}
35 />
36 );
37}
38
39export function CardDescription({
40 className,
41 ...props
42}: React.HTMLAttributes<HTMLParagraphElement>) {
43 return (
44 <p className={cn('text-sm text-gray-500', className)} {...props} />
45 );
46}
47
48export function CardContent({ className, ...props }: CardProps) {
49 return <div className={cn('p-6 pt-0', className)} {...props} />;
50}
51
52export function CardFooter({ className, ...props }: CardProps) {
53 return (
54 <div
55 className={cn('flex items-center p-6 pt-0', className)}
56 {...props}
57 />
58 );
59}
60
61// Usage
62<Card>
63 <CardHeader>
64 <CardTitle>Card Title</CardTitle>
65 <CardDescription>Card description goes here.</CardDescription>
66 </CardHeader>
67 <CardContent>
68 <p>Card content...</p>
69 </CardContent>
70 <CardFooter>
71 <Button>Action</Button>
72 </CardFooter>
73</Card>Input Component#
1// components/Input.tsx
2import { forwardRef } from 'react';
3import { cn } from '@/lib/utils';
4
5interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6 label?: string;
7 error?: string;
8 hint?: string;
9}
10
11export const Input = forwardRef<HTMLInputElement, InputProps>(
12 ({ className, label, error, hint, id, ...props }, ref) => {
13 const inputId = id || props.name;
14
15 return (
16 <div className="space-y-1">
17 {label && (
18 <label
19 htmlFor={inputId}
20 className="block text-sm font-medium text-gray-700"
21 >
22 {label}
23 {props.required && <span className="text-error ml-1">*</span>}
24 </label>
25 )}
26 <input
27 id={inputId}
28 className={cn(
29 'block w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors',
30 'placeholder:text-gray-400',
31 'focus:outline-none focus:ring-2 focus:ring-offset-0',
32 error
33 ? 'border-error focus:border-error focus:ring-error/20'
34 : 'border-gray-300 focus:border-brand-500 focus:ring-brand-500/20',
35 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500',
36 className
37 )}
38 ref={ref}
39 aria-invalid={error ? 'true' : 'false'}
40 aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
41 {...props}
42 />
43 {error && (
44 <p id={`${inputId}-error`} className="text-sm text-error">
45 {error}
46 </p>
47 )}
48 {hint && !error && (
49 <p id={`${inputId}-hint`} className="text-sm text-gray-500">
50 {hint}
51 </p>
52 )}
53 </div>
54 );
55 }
56);
57
58Input.displayName = 'Input';
59
60// Usage
61<Input
62 label="Email"
63 type="email"
64 placeholder="you@example.com"
65 required
66/>
67<Input
68 label="Password"
69 type="password"
70 error="Password must be at least 8 characters"
71/>Responsive Patterns#
1// Responsive container
2function Container({ children, className }: { children: React.ReactNode; className?: string }) {
3 return (
4 <div className={cn(
5 'mx-auto w-full px-4 sm:px-6 lg:px-8',
6 'max-w-7xl',
7 className
8 )}>
9 {children}
10 </div>
11 );
12}
13
14// Responsive grid
15function ResponsiveGrid({ children }: { children: React.ReactNode }) {
16 return (
17 <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
18 {children}
19 </div>
20 );
21}
22
23// Responsive stack
24function Stack({ children }: { children: React.ReactNode }) {
25 return (
26 <div className="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
27 {children}
28 </div>
29 );
30}
31
32// Hide/show based on breakpoint
33function ResponsiveNav() {
34 return (
35 <nav>
36 {/* Mobile menu button */}
37 <button className="md:hidden">
38 <MenuIcon />
39 </button>
40
41 {/* Desktop nav */}
42 <div className="hidden md:flex md:space-x-4">
43 <NavLinks />
44 </div>
45 </nav>
46 );
47}Animation Utilities#
1// tailwind.config.js
2module.exports = {
3 theme: {
4 extend: {
5 animation: {
6 'fade-in': 'fadeIn 0.3s ease-out',
7 'slide-up': 'slideUp 0.3s ease-out',
8 'slide-down': 'slideDown 0.3s ease-out',
9 'scale-in': 'scaleIn 0.2s ease-out',
10 },
11 keyframes: {
12 fadeIn: {
13 '0%': { opacity: '0' },
14 '100%': { opacity: '1' },
15 },
16 slideUp: {
17 '0%': { transform: 'translateY(10px)', opacity: '0' },
18 '100%': { transform: 'translateY(0)', opacity: '1' },
19 },
20 slideDown: {
21 '0%': { transform: 'translateY(-10px)', opacity: '0' },
22 '100%': { transform: 'translateY(0)', opacity: '1' },
23 },
24 scaleIn: {
25 '0%': { transform: 'scale(0.95)', opacity: '0' },
26 '100%': { transform: 'scale(1)', opacity: '1' },
27 },
28 },
29 },
30 },
31};
32
33// Usage
34<div className="animate-fade-in">Fading in...</div>
35<div className="animate-slide-up">Sliding up...</div>
36
37// With delays
38<div className="animate-fade-in animation-delay-100">Delayed</div>
39
40// Conditional animation
41<div className={cn(
42 'transition-all duration-300',
43 isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'
44)}>
45 Animated content
46</div>Dark Mode#
1// tailwind.config.js
2module.exports = {
3 darkMode: 'class', // or 'media'
4};
5
6// Component with dark mode
7function DarkModeCard() {
8 return (
9 <div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800">
10 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
11 Title
12 </h2>
13 <p className="mt-2 text-gray-600 dark:text-gray-300">
14 Description text
15 </p>
16 </div>
17 );
18}
19
20// Theme toggle
21function ThemeToggle() {
22 const [isDark, setIsDark] = useState(false);
23
24 useEffect(() => {
25 document.documentElement.classList.toggle('dark', isDark);
26 }, [isDark]);
27
28 return (
29 <button
30 onClick={() => setIsDark(!isDark)}
31 className="rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
32 >
33 {isDark ? <SunIcon /> : <MoonIcon />}
34 </button>
35 );
36}Best Practices#
Organization:
✓ Use cva for variant management
✓ Create cn() utility for merging
✓ Extract repeated patterns
✓ Keep components focused
Performance:
✓ Purge unused styles
✓ Use @apply sparingly
✓ Prefer composition over @apply
✓ Split large components
Maintainability:
✓ Use design tokens
✓ Document component variants
✓ Create a component library
✓ Test responsive breakpoints
Conclusion#
Tailwind CSS excels at component-driven development. Use class-variance-authority for variants, extract common patterns into components, and leverage design tokens for consistency. The utility-first approach scales well when combined with proper component architecture.