Back to Blog
TailwindCSSComponentsDesign Systems

Tailwind CSS Component Patterns

Build reusable components with Tailwind CSS. From design tokens to component variants to responsive patterns.

B
Bootspring Team
Engineering
December 14, 2021
6 min read

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.

Share this article

Help spread the word about Bootspring