Implementing a Theme Switcher

Learn how to implement a dynamic theme switcher in your application using Styleframe's data-theme attribute system.

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;

When you change the data-theme attribute, all CSS variables are updated automatically, cascading the new theme values throughout your application.

Pro tip: For a comprehensive guide on defining and working with themes, see the Themes API documentation.

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>
    );
}
Pro tip: All implementations include localStorage persistence so the user's theme preference is remembered across sessions.

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:

  1. Respecting explicit user choices (localStorage)
  2. Falling back to system preferences
  3. 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;
Good to know: The default theme (light) doesn't need to be explicitly defined since the initial variable values serve as the default. Only define themes that override the defaults.

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