DEV Community

Cover image for Master the Art of Personalization: Build a React & Tailwind Theme Switcher
Bishoy Semsem
Bishoy Semsem

Posted on

Master the Art of Personalization: Build a React & Tailwind Theme Switcher

Ever landed on a website late at night, only to be blinded by its default bright white interface? Or perhaps you're building an app and want to offer a premium, customizable feel? User personalization isn't just a "nice-to-have" anymore; it's an expectation. And one of the most impactful ways to offer it is through a theme switcher.

In my experience building enterprise dashboards, I've found that a well-implemented theme switcher significantly improves user satisfaction. Let me show you how to build one the right way.

Why This Matters in Real Projects

Imagine you're building the next big social media app. You've got sleek profiles, an intuitive feed, and a vibrant community. But then, users start asking: "Can we have a dark mode?"

This is where a well-architected theme switcher comes in. We'll leverage React's Context API to create a global theme state that any component can access, and Tailwind CSS's dark: variant to effortlessly apply theme-specific styles. We'll even persist the user's choice in localStorage, so their preference is remembered on subsequent visits.

Let's Build This Thing

Step 1: Set Up Your React & Tailwind Project

First things first, let's get a fresh React project with Tailwind CSS configured.

npx create-react-app theme-switcher-app
cd theme-switcher-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Now, configure tailwind.config.js to enable dark mode:

// tailwind.config.js
module.exports = {
  darkMode: 'class', // We'll toggle a 'dark' class on the HTML element
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Theme Context

Here's where the magic happens. We'll use React Context to manage our theme state globally.

// src/contexts/ThemeContext.tsx
import React, { createContext, useState, useEffect, useContext } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>(() => {
    // Check localStorage first, then system preference
    const stored = localStorage.getItem('theme') as Theme;
    if (stored) return stored;

    return window.matchMedia('(prefers-color-scheme: dark)').matches 
      ? 'dark' 
      : 'light';
  });

  useEffect(() => {
    const root = document.documentElement;
    root.classList.remove('light', 'dark');
    root.classList.add(theme);
    localStorage.setItem('theme', theme);
  }, [theme]);

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Build the Theme Switcher Component

// src/components/ThemeSwitcher.tsx
import { useTheme } from '../contexts/ThemeContext';

const ThemeSwitcher = () => {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      className="p-2 rounded-full bg-gray-200 dark:bg-gray-800 
                 text-gray-800 dark:text-gray-200 
                 shadow-md transition-colors duration-300"
      aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
    >
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  );
};

export default ThemeSwitcher;
Enter fullscreen mode Exit fullscreen mode

Step 4: Put It All Together

// src/App.tsx
import { ThemeProvider } from './contexts/ThemeContext';
import ThemeSwitcher from './components/ThemeSwitcher';

function App() {
  return (
    <ThemeProvider>
      <div className="min-h-screen bg-gray-100 dark:bg-gray-900 
                      text-gray-900 dark:text-gray-100 
                      transition-colors duration-300">
        <nav className="p-4 flex justify-end">
          <ThemeSwitcher />
        </nav>

        <main className="container mx-auto p-8">
          <h1 className="text-4xl font-bold mb-4">
            Welcome to My Themed App
          </h1>
          <p className="text-lg text-gray-600 dark:text-gray-400">
            Toggle the theme and watch the magic happen.
          </p>
        </main>
      </div>
    </ThemeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

What Most Tutorials Miss

Here's something I learned the hard way: always respect system preferences first. Notice how our context checks window.matchMedia('(prefers-color-scheme: dark)') before defaulting? This small detail makes your app feel native and respectful of user settings.

Another pitfall: don't forget the transition-colors duration-300 class. Without smooth transitions, theme switching feels jarring and unprofessional.

Performance Considerations

One thing to watch out for: if you're using server-side rendering (Next.js, for example), you'll get a flash of unstyled content (FOUC) because localStorage isn't available on the server. The solution? Add a small script in your <head> that runs before React hydrates:

<script>
  const theme = localStorage.getItem('theme') || 
    (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.classList.add(theme);
</script>
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Use React Context for global theme state — it's cleaner than prop drilling
  • Tailwind's dark: variant makes theming almost effortless
  • Always persist preferences in localStorage for returning users
  • Respect system preferences as the initial default
  • Add smooth transitions for a polished feel

Now go ahead and give your users the power to choose. They'll thank you for it.


✨ Let's keep the conversation going!

If you found this interesting, I'd love for you to check out more of my work or just drop in to say hello.

✍️ Read more on my blog: bishoy-bishai.github.io

Let's chat on LinkedIn: linkedin.com/in/bishoybishai


Top comments (0)