Skip to content

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.

Terminal window
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// 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 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 component
const 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

For more complex conditional styling, use a utility function to combine classes cleanly.

// utils/cn.js
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
// components/Button.jsx
import { 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 elegantly
  • twMerge prevents Tailwind class conflicts by merging duplicate utilities
  • Allows component consumers to override styles
  • Maintains clean, readable component code

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.js
export 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

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.

Terminal window
npm install next-themes
// App.jsx
import { 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;
// components/ThemeToggle.jsx
import { 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 the dark class on the HTML element
  • defaultTheme="system" respects user’s system preference by default
  • enableSystem allows automatic switching based on system settings
  • Mounted check prevents hydration mismatches in SSR environments

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

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.

For smooth, performant animations, combine Tailwind’s transition utilities with Framer Motion for complex interactions:

Terminal window
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
  1. Composition over Configuration: Build flexible components using children and composition
  2. Consistent Naming: Use semantic class names and consistent naming patterns
  3. Prop-Based Variants: Handle component variations through props, not hardcoded classes
  4. Accessibility First: Always include focus states, ARIA labels, and keyboard navigation
  1. Utility Classes: Group related utilities together in readable chunks
  2. Custom Components: Extract repeated patterns into reusable components
  3. CSS Variables: Use CSS variables for dynamic theming and consistent design tokens
  4. Layer Organization: Use Tailwind’s layer system (@layer base, components, utilities)
  1. Purge Unused CSS: Configure content paths correctly to remove unused styles
  2. Critical CSS: Load essential styles first, defer non-critical styles
  3. Bundle Splitting: Split CSS by route or component for better loading
  4. Minimize Custom CSS: Leverage Tailwind utilities over custom CSS when possible