Step-by-Step Theme Implementation Guide

1. Set up Theme Structure

First, create a theme structure:

// theme/base.ts
export const baseTheme = {
  spacing: 8,
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
  // Common theme properties
};

// theme/light.ts
import { baseTheme } from './base';

export const lightTheme = {
  ...baseTheme,
  palette: {
    mode: 'light' as const,
    primary: {
      main: '#1976d2',
    },
    background: {
      default: '#ffffff',
      paper: '#f5f5f5',
    },
    text: {
      primary: '#000000',
      secondary: '#666666',
    },
  },
};

// theme/dark.ts
import { baseTheme } from './base';

export const darkTheme = {
  ...baseTheme,
  palette: {
    mode: 'dark' as const,
    primary: {
      main: '#90caf9',
    },
    background: {
      default: '#121212',
      paper: '#1e1e1e',
    },
    text: {
      primary: '#ffffff',
      secondary: '#b3b3b3',
    },
  },
};

// theme/index.ts
export { lightTheme } from './light';
export { darkTheme } from './dark';
export { baseTheme } from './base';

2. Create a Theme Store with Zustand

Create a theme store:

// hooks/useTheme.tsx
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { lightTheme, darkTheme } from '../theme';

type ThemeMode = 'light' | 'dark';

interface ThemeState {
  mode: ThemeMode;
  theme: typeof lightTheme | typeof darkTheme;
  toggleTheme: () => void;
  setTheme: (mode: ThemeMode) => void;
}

export const useTheme = create<ThemeState>()(
  persist(
    (set, get) => ({
      mode: 'light',
      theme: lightTheme,
      toggleTheme: () => {
        const currentMode = get().mode;
        const newMode = currentMode === 'light' ? 'dark' : 'light';
        set({
          mode: newMode,
          theme: newMode === 'light' ? lightTheme : darkTheme,
        });
      },
      setTheme: (mode: ThemeMode) => {
        set({
          mode,
          theme: mode === 'light' ? lightTheme : darkTheme,
        });
      },
    }),
    {
      name: `${window.appName || 'app'}-theme-storage`,
      partialize: (state) => ({ mode: state.mode }),
    }
  )
);

3. Create Theme Provider Component

// components/ThemeProvider.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { createTheme } from '@mui/material/styles';
import { useTheme } from '../hooks/useTheme';

const ThemeContext = createContext(null);

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const { theme } = useTheme();
  
  const muiTheme = createTheme(theme);

  return (
    <MuiThemeProvider theme={muiTheme}>
      <CssBaseline />
      {children}
    </MuiThemeProvider>
  );
};

4. Create Theme Toggle Component

// components/ThemeToggle.tsx
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import { Brightness4, Brightness7 } from '@mui/icons-material';
import { useTheme } from '../hooks/useTheme';

export const ThemeToggle: React.FC = () => {
  const { mode, toggleTheme } = useTheme();

  return (
    <Tooltip title={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}>
      <IconButton onClick={toggleTheme} color="inherit">
        {mode === 'light' ? <Brightness4 /> : <Brightness7 />}
      </IconButton>
    </Tooltip>
  );
};

5. Set up Global App Name

In your main.tsx file:

// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

// Set the app name for theme storage
declare global {
  interface Window {
    appName: string;
  }
}

window.appName = 'your-app-name';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

6. Wrap Your App with Theme Provider

// App.tsx
import React from 'react';
import { ThemeProvider } from './components/ThemeProvider';
import { ThemeToggle } from './components/ThemeToggle';
import { AppBar, Toolbar, Typography, Box } from '@mui/material';

function App() {
  return (
    <ThemeProvider>
      <Box sx={{ flexGrow: 1 }}>
        <AppBar position="static">
          <Toolbar>
            <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
              My App
            </Typography>
            <ThemeToggle />
          </Toolbar>
        </AppBar>
        {/* Your app content */}
        <Box sx={{ p: 3 }}>
          <Typography variant="h4">Welcome to My App</Typography>
          <Typography variant="body1">
            This app supports dark and light themes!
          </Typography>
        </Box>
      </Box>
    </ThemeProvider>
  );
}

export default App;

7. Install Required Dependencies

npm install zustand @mui/material @mui/icons-material @emotion/react @emotion/styled

8. Add CSS Variables (Optional)

For additional CSS customization, you can also add CSS variables:

/* index.css */
:root {
  --primary-color: #1976d2;
  --background-color: #ffffff;
  --text-color: #000000;
}

[data-theme='dark'] {
  --primary-color: #90caf9;
  --background-color: #121212;
  --text-color: #ffffff;
}

9. Advanced: Theme Persistence with System Preference

// hooks/useTheme.tsx (enhanced version)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { lightTheme, darkTheme } from '../theme';

type ThemeMode = 'light' | 'dark' | 'system';

interface ThemeState {
  mode: ThemeMode;
  effectiveMode: 'light' | 'dark';
  theme: typeof lightTheme | typeof darkTheme;
  toggleTheme: () => void;
  setTheme: (mode: ThemeMode) => void;
}

const getSystemTheme = (): 'light' | 'dark' => {
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};

const getEffectiveMode = (mode: ThemeMode): 'light' | 'dark' => {
  return mode === 'system' ? getSystemTheme() : mode;
};

export const useTheme = create<ThemeState>()(
  persist(
    (set, get) => ({
      mode: 'system',
      effectiveMode: getSystemTheme(),
      theme: getSystemTheme() === 'light' ? lightTheme : darkTheme,
      toggleTheme: () => {
        const currentMode = get().mode;
        const newMode = currentMode === 'light' ? 'dark' : 'light';
        const effectiveMode = getEffectiveMode(newMode);
        set({
          mode: newMode,
          effectiveMode,
          theme: effectiveMode === 'light' ? lightTheme : darkTheme,
        });
      },
      setTheme: (mode: ThemeMode) => {
        const effectiveMode = getEffectiveMode(mode);
        set({
          mode,
          effectiveMode,
          theme: effectiveMode === 'light' ? lightTheme : darkTheme,
        });
      },
    }),
    {
      name: `${window.appName || 'app'}-theme-storage`,
      partialize: (state) => ({ mode: state.mode }),
    }
  )
);

Key Benefits of This Approach:

  1. Persistent Theme: Uses Zustand with persistence to remember user preference
  2. Material-UI Integration: Seamlessly works with MUI components
  3. Type Safety: Full TypeScript support
  4. Flexible: Easy to extend with more themes or custom properties
  5. Performance: Minimal re-renders with Zustand
  6. System Integration: Optional system theme preference support

Leave a comment