UI & Styling
Modern React applications require efficient styling approaches that scale well and provide excellent developer experience. This guide covers Tailwind CSS fundamentals, dark mode implementation, and styling best practices.
Tailwind CSS Setup
Section titled “Tailwind CSS Setup”Installation
Section titled “Installation”npm install -D tailwindcss postcss autoprefixernpx tailwindcss init -p
Basic Configuration
Section titled “Basic Configuration”// tailwind.config.js/** @type {import('tailwindcss').Config} */export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [],}
/* src/index.css */@import "tailwindcss";
Key Configuration Points:
- Using the latest
@import "tailwindcss"
syntax instead of separate layer imports content
array tells Tailwind which files to scan for class names- Start with minimal configuration and extend as needed
extend
preserves default Tailwind values while adding custom ones- Custom colors follow Tailwind’s numeric scale (50-950)
- Font families should include fallbacks for better performance
Utility-First Class Organization
Section titled “Utility-First Class Organization”Understanding Utility-First Approach
Section titled “Understanding Utility-First Approach”Utility-first means building designs by applying small, single-purpose utility classes directly in your markup. This approach provides incredible flexibility and maintainability.
// Instead of writing custom CSS for each componentconst Button = ({ children }) => { return ( <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors"> {children} </button> );};
const Card = ({ title, content }) => { return ( <div className="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden"> <div className="p-6"> <h2 className="text-xl font-bold text-gray-900 mb-2">{title}</h2> <p className="text-gray-600">{content}</p> </div> </div> );};
Why Utility-First Works:
- No context switching: Style directly in your components without jumping between files
- No naming conflicts: Utilities are globally consistent and predictable
- Faster development: No time spent thinking about class names or CSS architecture
- Easier maintenance: Changes are localized and visible immediately
- Better performance: CSS bundle size grows sub-linearly with your project
Class Composition with clsx/cn
Section titled “Class Composition with clsx/cn”For more complex conditional styling, use a utility function to combine classes cleanly.
// utils/cn.jsimport { clsx } from 'clsx';import { twMerge } from 'tailwind-merge';
export function cn(...inputs) { return twMerge(clsx(inputs));}
// components/Button.jsximport { cn } from '@utils/cn';
const Button = ({ variant = 'primary', className, children, ...props }) => { return ( <button className={cn( // Base styles always applied "inline-flex items-center justify-center px-4 py-2 rounded-md font-medium transition-colors", // Conditional variant styles { "bg-blue-600 text-white hover:bg-blue-700": variant === 'primary', "bg-gray-200 text-gray-900 hover:bg-gray-300": variant === 'secondary', }, // Allow custom overrides className )} {...props} > {children} </button> );};
Benefits of this approach:
clsx
handles conditional classes elegantlytwMerge
prevents Tailwind class conflicts by merging duplicate utilities- Allows component consumers to override styles
- Maintains clean, readable component code
Global Styles with CSS Variables
Section titled “Global Styles with CSS Variables”Design System with CSS Variables
Section titled “Design System with CSS Variables”CSS variables provide a powerful way to create consistent design systems that work seamlessly with Tailwind. They enable dynamic theming and ensure consistency across your application.
/* src/styles/globals.css */@import "tailwindcss";
@layer base { :root { /* GDG Brand Colors */ --gdg-yellow-primary: 251 188 52; /* #FBBC34 - Main GDG Yellow */ --gdg-blue-primary: 66 133 244; /* #4285F4 - Google Blue */ --gdg-red-primary: 234 67 53; /* #EA4335 - Google Red */ --gdg-green-primary: 52 168 83; /* #34A853 - Google Green */
/* Typography System */ --gdg-font-main: 'Google Sans', system-ui, sans-serif; --gdg-font-mono: 'Google Sans Mono', 'Fira Code', monospace;
/* Semantic Colors */ --gdg-color-background: 255 255 255; --gdg-color-foreground: 32 33 36; /* Google's dark gray */ --gdg-color-muted: 95 99 104; /* Google's medium gray */ --gdg-color-border: 218 220 224; /* Google's light gray */
/* Component Specific */ --gdg-card-shadow: 0 1px 3px rgba(60, 64, 67, 0.3); --gdg-header-height: 4rem; }
.dark { --gdg-color-background: 32 33 36; --gdg-color-foreground: 248 249 250; --gdg-color-muted: 154 160 166; --gdg-color-border: 95 99 104; --gdg-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); }}
Update Tailwind Config:
// tailwind.config.jsexport default { content: ["./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { colors: { gdg: { yellow: "rgb(var(--gdg-yellow-primary))", blue: "rgb(var(--gdg-blue-primary))", red: "rgb(var(--gdg-red-primary))", green: "rgb(var(--gdg-green-primary))", }, background: "rgb(var(--gdg-color-background))", foreground: "rgb(var(--gdg-color-foreground))", muted: "rgb(var(--gdg-color-muted))", border: "rgb(var(--gdg-color-border))", }, fontFamily: { main: "var(--gdg-font-main)", mono: "var(--gdg-font-mono)", }, boxShadow: { gdg: "var(--gdg-card-shadow)", }, }, },}
Why This Approach Works:
- Brand Consistency: GDG-specific variables ensure consistent brand colors across the application
- Semantic Naming: Variables like
--gdg-yellow-primary
are self-documenting and meaningful - Dynamic Theming: CSS variables can be changed at runtime for theme switching
- Design System Integration: Variables act as a single source of truth for design tokens
- Developer Experience: Meaningful names make it easier to choose the right color/font
Usage Example:
const EventCard = ({ title, date, location }) => { return ( <div className="bg-background border border-border rounded-lg shadow-gdg p-6"> <h3 className="font-main text-xl font-bold text-foreground mb-2">{title}</h3> <div className="flex items-center gap-2 text-muted"> <span className="bg-gdg-yellow text-black px-2 py-1 rounded text-sm font-medium"> {date} </span> <span className="font-mono text-sm">{location}</span> </div> </div> );};
Benefits of CSS Variables:
- Consistent design tokens across the application
- Easy theme switching (light/dark mode)
- Dynamic color manipulation with JavaScript
- Better performance than CSS-in-JS solutions
Dark Mode Implementation
Section titled “Dark Mode Implementation”Using next-themes (Recommended)
Section titled “Using next-themes (Recommended)”While next-themes was originally built for Next.js, it works perfectly with React applications and provides a much simpler setup than custom context solutions.
npm install next-themes
Setup with next-themes
Section titled “Setup with next-themes”// App.jsximport { ThemeProvider } from 'next-themes';import ThemeToggle from '@components/ThemeToggle';
function App() { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> <div className="min-h-screen bg-background text-foreground transition-colors"> <header className="border-b border-border"> <div className="container mx-auto flex items-center justify-between px-4 py-4"> <h1 className="text-2xl font-bold">GDG App</h1> <ThemeToggle /> </div> </header>
<main className="container mx-auto px-4 py-8"> {/* Your app content */} </main> </div> </ThemeProvider> );}
export default App;
Theme Toggle Component
Section titled “Theme Toggle Component”// components/ThemeToggle.jsximport { useTheme } from 'next-themes';import { useEffect, useState } from 'react';import { Moon, Sun, Monitor } from 'lucide-react';
const ThemeToggle = () => { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false);
// Avoid hydration mismatch useEffect(() => { setMounted(true); }, []);
if (!mounted) { return null; // Avoid hydration issues }
return ( <div className="flex items-center gap-1 p-1 bg-muted rounded-lg"> <button onClick={() => setTheme('light')} className={`p-2 rounded-md transition-colors ${ theme === 'light' ? 'bg-background shadow-sm' : 'hover:bg-background/50' }`} aria-label="Light mode" > <Sun className="h-4 w-4" /> </button>
<button onClick={() => setTheme('dark')} className={`p-2 rounded-md transition-colors ${ theme === 'dark' ? 'bg-background shadow-sm' : 'hover:bg-background/50' }`} aria-label="Dark mode" > <Moon className="h-4 w-4" /> </button>
<button onClick={() => setTheme('system')} className={`p-2 rounded-md transition-colors ${ theme === 'system' ? 'bg-background shadow-sm' : 'hover:bg-background/50' }`} aria-label="System theme" > <Monitor className="h-4 w-4" /> </button> </div> );};
export default ThemeToggle;
Why next-themes is Better:
- Zero Configuration: Works out of the box with sensible defaults
- System Theme Support: Automatically respects user’s OS preference
- SSR Safe: Prevents hydration mismatches in SSR applications
- Persistent: Automatically saves theme preference to localStorage
- Multiple Themes: Supports more than just light/dark (system, auto, custom themes)
- Performance: Optimized for minimal re-renders and smooth transitions
Key Features:
attribute="class"
tells next-themes to toggle thedark
class on the HTML elementdefaultTheme="system"
respects user’s system preference by defaultenableSystem
allows automatic switching based on system settings- Mounted check prevents hydration mismatches in SSR environments
Advanced Styling Patterns
Section titled “Advanced Styling Patterns”Responsive Design
Section titled “Responsive Design”Responsive Strategy:
- Mobile-first approach with progressive enhancement
- Consistent breakpoint usage (sm, md, lg, xl, 2xl)
- Flexible grid systems that adapt to screen size
- Proportional spacing and typography scaling
Component Composition
Section titled “Component Composition”Build flexible components using composition patterns. Create base components with sensible defaults and allow customization through props and children. This approach scales better than creating multiple variant components.
Animation & Transitions
Section titled “Animation & Transitions”For smooth, performant animations, combine Tailwind’s transition utilities with Framer Motion for complex interactions:
npm install framer-motion
Framer Motion excels at:
- Page transitions and route animations
- Complex gesture handling (drag, hover, tap)
- Staggered animations and orchestration
- Layout animations and auto-animated layout changes
- Advanced spring physics and easing
For simpler animations, Tailwind’s built-in transitions and transforms work perfectly:
- Hover states and focus transitions
- Basic slide, fade, and scale effects
- Loading spinners and progress indicators
When to use what:
- Tailwind transitions: Simple hover effects, focus states, basic UI feedback
- Framer Motion: Complex interactions, page transitions, data visualization animations
- Custom CSS: Highly specific animations that need fine-grained control
Best Practices
Section titled “Best Practices”Component Design
Section titled “Component Design”- Composition over Configuration: Build flexible components using children and composition
- Consistent Naming: Use semantic class names and consistent naming patterns
- Prop-Based Variants: Handle component variations through props, not hardcoded classes
- Accessibility First: Always include focus states, ARIA labels, and keyboard navigation
Code Organization
Section titled “Code Organization”- Utility Classes: Group related utilities together in readable chunks
- Custom Components: Extract repeated patterns into reusable components
- CSS Variables: Use CSS variables for dynamic theming and consistent design tokens
- Layer Organization: Use Tailwind’s layer system (@layer base, components, utilities)
Performance
Section titled “Performance”- Purge Unused CSS: Configure content paths correctly to remove unused styles
- Critical CSS: Load essential styles first, defer non-critical styles
- Bundle Splitting: Split CSS by route or component for better loading
- Minimize Custom CSS: Leverage Tailwind utilities over custom CSS when possible