Global Theme Management
Learn how to implement a global theme system in Refract using refractions and optics for consistent styling across your application.
Overview
This tutorial demonstrates how to create a flexible theme system that allows users to switch between light and dark modes, customize colors, and maintain theme persistence across sessions.
What You'll Build
- A global theme provider using refractions
- Theme switching functionality
- Persistent theme storage
- Dynamic CSS custom properties
- Theme-aware components
Prerequisites
- Basic understanding of Refract components and refractions
- Familiarity with CSS custom properties
- Knowledge of localStorage API
Step 1: Create the Theme Store
First, let's create a global theme store using refractions:
// theme/themeStore.js
import { createRefraction } from 'refract-js';
// Define available themes
export const themes = {
light: {
name: 'light',
colors: {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
surface: '#f8f9fa',
text: '#212529',
textSecondary: '#6c757d',
border: '#dee2e6',
shadow: 'rgba(0, 0, 0, 0.1)',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
},
dark: {
name: 'dark',
colors: {
primary: '#0d6efd',
secondary: '#6c757d',
background: '#121212',
surface: '#1e1e1e',
text: '#ffffff',
textSecondary: '#adb5bd',
border: '#343a40',
shadow: 'rgba(0, 0, 0, 0.3)',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
},
};
// Load theme from localStorage or default to light
const getInitialTheme = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('refract-theme');
return saved && themes[saved] ? themes[saved] : themes.light;
}
return themes.light;
};
// Create global theme refraction
export const themeStore = createRefraction(getInitialTheme());
Step 2: Create Theme Provider Component
Now let's create a theme provider that applies CSS custom properties:
// theme/ThemeProvider.js
import { createComponent } from 'refract-js';
import { themeStore } from './themeStore.js';
const ThemeProvider = createComponent(({ lens, children }) => {
const theme = lens.useRefraction(themeStore);
// Apply theme to CSS custom properties
lens.useEffect(() => {
const root = document.documentElement;
const currentTheme = theme.value;
// Apply color variables
Object.entries(currentTheme.colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value);
});
// Apply spacing variables
Object.entries(currentTheme.spacing).forEach(([key, value]) => {
root.style.setProperty(`--spacing-${key}`, value);
});
// Apply border radius variables
Object.entries(currentTheme.borderRadius).forEach(([key, value]) => {
root.style.setProperty(`--radius-${key}`, value);
});
// Add theme class to body
document.body.className = `theme-${currentTheme.name}`;
// Persist theme choice
localStorage.setItem('refract-theme', currentTheme.name);
}, [theme.value]);
return (
<div className="theme-provider">
{children}
</div>
);
});
export default ThemeProvider;
Step 3: Create Theme Utilities
Let's create utility functions and hooks for working with themes:
// theme/useTheme.js
import { createOptic } from 'refract-js';
import { themeStore, themes } from './themeStore.js';
export const useTheme = createOptic((lens) => {
const theme = lens.useRefraction(themeStore);
const setTheme = (themeName) => {
if (themes[themeName]) {
theme.set(themes[themeName]);
}
};
const toggleTheme = () => {
const current = theme.value.name;
const next = current === 'light' ? 'dark' : 'light';
setTheme(next);
};
const isDark = theme.value.name === 'dark';
const isLight = theme.value.name === 'light';
return {
theme: theme.value,
setTheme,
toggleTheme,
isDark,
isLight,
availableThemes: Object.keys(themes),
};
});
Step 4: Create Theme Switcher Component
Now let's build a component for switching themes:
// components/ThemeSwitcher.js
import { createComponent } from 'refract-js';
import { useTheme } from '../theme/useTheme.js';
const ThemeSwitcher = createComponent(({ lens }) => {
const { theme, toggleTheme, isDark } = useTheme(lens);
return (
<button
className="theme-switcher"
onClick={toggleTheme}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} theme`}
>
<span className="theme-icon">
{isDark ? '