Dynamic Theme Engine in Next.js

A comprehensive guide to implementing a flexible, server-side theme management system in Next.js
November 26, 2024

Why Another Theme Management Solution?

When building modern web applications, theme management can quickly become complex:

  • Seamless dark/light mode switching
  • Server-side theme persistence
  • Instant UI updates
  • Accessibility considerations
  • Performance optimization

Our implementation solves these challenges while providing a delightful developer experience.

The Gist

1// Basic usage
2const { theme, selectTheme, toggleMode } = useTheme('light');
3
4// Selecting a specific theme
5selectTheme('dark');
6
7// Toggling between light and dark
8toggleMode();
9

Implementation Details

1. Theme Hook Design

Our theme management system is built around a flexible and performant hook that handles theme selection, persistence, and UI updates.

1function useTheme(currentThemeName: Theme) {
2 const router = useRouter();
3 const [theme, setTheme] = useState<Theme>(currentThemeName);
4
5 const selectTheme = async (theme: Theme) => {
6 // Update body attribute for CSS targeting
7 document.body.setAttribute("data-theme", theme);
8
9 // Persist theme server-side
10 await setCurrentTheme(theme);
11
12 // Refresh server-side rendering
13 router.refresh();
14
15 // Update local state
16 setTheme(theme);
17 };
18
19 const toggleMode = async () => {
20 const selectedDark = theme !== "light";
21 const newTheme = selectedDark ? "light" : "dark";
22
23 // Update UI immediately
24 document.body.setAttribute("data-theme", newTheme);
25
26 // Update local state
27 setTheme(newTheme);
28
29 // Persist theme change
30 await toggleDarkTheme();
31
32 // Refresh server-side rendering
33 router.refresh();
34 };
35
36 return { theme, selectTheme, toggleMode };
37}
38

Key Design Principles

  1. Immediate UI Feedback

    • document.body.setAttribute() ensures instant visual changes
    • Local state updates provide responsive UI
  2. Server-Side Persistence

    • setCurrentTheme() and toggleDarkTheme() handle server-side storage
    • router.refresh() ensures server-side rendering reflects the new theme
  3. Async Operations

    • Theme changes are handled asynchronously
    • Prevents blocking of UI interactions

CSS Integration

1/* theme.css */
2[data-theme='light'] {
3 --background: white;
4 --text: black;
5}
6
7[data-theme='dark'] {
8 --background: black;
9 --text: white;
10}
11
12body {
13 background-color: var(--background);
14 color: var(--text);
15 transition: background-color 0.3s, color 0.3s;
16}
17

Theme List Component

1function ThemeList() {
2 const { theme, selectTheme } = useTheme('light');
3
4 const themes = [
5 { name: 'light', label: 'Light Mode' },
6 { name: 'dark', label: 'Dark Mode' },
7 { name: 'system', label: 'System Preference' }
8 ];
9
10 return (
11 <div className="theme-switcher">
12 {themes.map(({ name, label }) => (
13 <button
14 key={name}
15 onClick={() => selectTheme(name)}
16 aria-pressed={theme === name}
17 >
18 {label}
19 </button>
20 ))}
21 </div>
22 );
23}
24

Performance Considerations

  • Minimal state management overhead
  • No unnecessary re-renders
  • Server-side theme persistence
  • Lightweight CSS variable approach

Accessibility Features

  • ARIA attributes for theme buttons
  • Respects user's system preferences
  • Smooth transitions between themes

Conclusion

Our theme engine provides:

  • Instant theme switching
  • Server-side persistence
  • Minimal performance impact
  • Enhanced user experience

Written with the help of WindSurf's Cascade.