Implementing a Theme Switcher
Overview
A theme switcher allows users to toggle between different visual themes in your application, such as light and dark modes. Styleframe makes this easy by using the data-theme
attribute on the <html>
element, which you can change dynamically with JavaScript to switch between themes instantly.
Why use a theme switcher?
Theme switching provides:
- Enhanced user experience: Let users choose their preferred visual style.
- Accessibility benefits: Dark mode can reduce eye strain in low-light environments.
- Modern expectations: Users expect apps to respect their system preferences and offer theme choices.
- Seamless transitions: Switch themes without page reloads or flickering.
How it Works
Styleframe uses the data-theme
attribute to scope theme-specific styles. When you define a theme, you use a callback function that receives a context object to override variables:
import { styleframe } from 'styleframe';
const s = styleframe();
const { variable, theme, ref, selector } = s;
// Define theme variables
const backgroundColor = variable('bg-color', '#ffffff');
const textColor = variable('text-color', '#000000');
// Apply variables to elements
selector('body', {
backgroundColor: ref(backgroundColor),
color: ref(textColor),
});
// Dark theme
theme('dark', (ctx) => {
ctx.variable(backgroundColor, '#1a1a1a');
ctx.variable(textColor, '#ffffff');
});
export default s;
:root {
--bg-color: #ffffff;
--text-color: #000000;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
}
When you change the data-theme
attribute, all CSS variables are updated automatically, cascading the new theme values throughout your application.
Implementation
To switch themes dynamically, update the data-theme
attribute on the <html>
element using JavaScript. Below are framework-specific implementations that include localStorage persistence and type safety.
import { useState, useEffect, useCallback } from 'react';
type Theme = 'light' | 'dark';
export const useTheme = () => {
const [theme, setThemeState] = useState<Theme>('light');
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
}, []);
const toggleTheme = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}, [theme, setTheme]);
useEffect(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('theme') as Theme;
if (saved && (saved === 'light' || saved === 'dark')) {
setTheme(saved);
}
}
}, [setTheme]);
return {
theme,
setTheme,
toggleTheme
} as const;
};
Usage:
import { useTheme } from './hooks/useTheme';
function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
</button>
);
}
import { ref, onMounted, readonly } from 'vue';
type Theme = 'light' | 'dark';
export const useTheme = () => {
const theme = ref<Theme>('light');
const setTheme = (newTheme: Theme) => {
theme.value = newTheme;
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
}
const toggleTheme = () => {
setTheme(theme.value === 'dark' ? 'light' : 'dark')
}
onMounted(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('theme') as Theme;
if (saved && (saved === 'light' || saved === 'dark')) {
setTheme(saved);
}
}
});
return {
theme: readonly(theme),
setTheme,
toggleTheme
}
}
Usage:
<script setup>
import { useTheme } from './composables/useTheme';
const { theme, toggleTheme } = useTheme();
</script>
<template>
<button @click="toggleTheme">
{{ theme === 'dark' ? '☀️ Light' : '🌙 Dark' }}
</button>
</template>
type Theme = 'light' | 'dark';
let currentTheme: Theme = 'light';
const initializeTheme = (): void => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('theme') as Theme;
if (saved && (saved === 'light' || saved === 'dark')) {
currentTheme = saved;
}
document.documentElement.setAttribute('data-theme', currentTheme);
}
};
export const getTheme = (): Theme => currentTheme;
export const setTheme = (newTheme: Theme): void => {
currentTheme = newTheme;
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
};
export const toggleTheme = (): void => {
setTheme(currentTheme === 'dark' ? 'light' : 'dark');
};
// Initialize on load
if (typeof window !== 'undefined') {
initializeTheme();
}
Usage:
import { getTheme, toggleTheme } from './theme';
const button = document.querySelector('#theme-toggle');
button?.addEventListener('click', () => {
toggleTheme();
button.textContent = getTheme() === 'dark' ? '☀️ Light' : '🌙 Dark';
});
Examples
Respecting System Preferences
Detect and apply the user's system theme preference on first load:
const getInitialTheme = (): Theme => {
// Check localStorage first
const saved = localStorage.getItem('theme') as Theme;
if (saved && (saved === 'light' || saved === 'dark')) {
return saved;
}
// Fall back to system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
// Use in your initialization
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
This approach provides the best user experience by:
- Respecting explicit user choices (localStorage)
- Falling back to system preferences
- Defaulting to light mode as a final fallback
Multiple Theme Support
Extend your theme switcher to support more than two themes:
import { styleframe } from 'styleframe';
const s = styleframe();
const { variable, theme, ref, selector } = s;
const backgroundColor = variable('bg-color', '#ffffff');
const textColor = variable('text-color', '#000000');
selector('body', {
backgroundColor: ref(backgroundColor),
color: ref(textColor),
});
theme('dark', (ctx) => {
ctx.variable(backgroundColor, '#1a1a1a');
ctx.variable(textColor, '#ffffff');
});
theme('sepia', (ctx) => {
ctx.variable(backgroundColor, '#f4ecd8');
ctx.variable(textColor, '#5c4e3a');
});
theme('high-contrast', (ctx) => {
ctx.variable(backgroundColor, '#000000');
ctx.variable(textColor, '#ffffff');
});
export default s;
type Theme = 'light' | 'dark' | 'sepia' | 'high-contrast';
export const setTheme = (newTheme: Theme): void => {
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
};
Smooth Theme Transitions
Add smooth transitions when switching themes to enhance the user experience:
import { styleframe } from 'styleframe';
const s = styleframe();
const { selector, variable, ref } = s;
const backgroundColor = variable('bg-color', '#ffffff');
const textColor = variable('text-color', '#000000');
selector('body', {
backgroundColor: ref(backgroundColor),
color: ref(textColor),
transition: 'background-color 0.3s ease, color 0.3s ease',
});
export default s;
Be cautious with transitions on all elements, as it can cause performance issues. Apply transitions selectively to specific properties like background-color
and color
.
Preventing Flash of Unstyled Content
Prevent the flash of the wrong theme on page load by initializing the theme before the page renders:
<!DOCTYPE html>
<html>
<head>
<script>
// Inline script runs before page render
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<!-- Your styles -->
</head>
<body>
<!-- Your content -->
</body>
</html>
This inline script runs synchronously before the page renders, preventing any flash of the wrong theme. It's small enough that it won't impact performance.
Best Practices
- Persist user preferences using localStorage or cookies to remember the theme across sessions.
- Respect system preferences by checking
prefers-color-scheme
media query as a default. - Provide visual feedback when switching themes, such as smooth transitions or loading states.
- Test both themes thoroughly to ensure all UI components are readable and accessible in each theme.
- Consider accessibility by ensuring sufficient color contrast in all themes, especially for text.
- Initialize early to prevent flash of unstyled content on page load.
FAQ
data-theme
attribute can be set to any string value you define.data-theme
attribute updates CSS variables instantly without any page reload. The browser re-evaluates the CSS and applies new values immediately.<head>
that sets the data-theme
attribute before the page renders. This script should run synchronously before your CSS loads.background-color
and color
. Be selective to avoid performance issues.You can use the code below to listen for system theme changes and update your theme accordingly.
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
});