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:
- Persistent Theme: Uses Zustand with persistence to remember user preference
- Material-UI Integration: Seamlessly works with MUI components
- Type Safety: Full TypeScript support
- Flexible: Easy to extend with more themes or custom properties
- Performance: Minimal re-renders with Zustand
- System Integration: Optional system theme preference support