Theme Manager

Prev Next

Theme Manager

The ThemeManager is a static utility class that handles theme computation, application, and persistence for the CMS interface. It supports three modes — light, dark, and system — and resolves the system preference against the browser's prefers-color-scheme media query.

Theme Modes

type ThemeType = 'light' | 'dark' | 'system';
Mode Behavior
light Forces light theme regardless of OS preference
dark Forces dark theme regardless of OS preference
system Resolves to light or dark based on prefers-color-scheme: dark

A pre-built options array is exported for use in UI selectors:

const ThemeOptions: OptionType<ThemeType>[] = [
  { label: 'Light', value: 'light' },
  { label: 'Dark', value: 'dark' },
  { label: 'System', value: 'system' },
];

Theme Toggle Logic

computeNextTheme(current, prefersDark) cycles through themes with a single-click toggle pattern:

Image

The key insight: when the current theme is system, toggling flips away from the system preference so the user sees an immediate visual change. If the OS prefers dark and the user clicks toggle, they get light — not the visually identical dark.

Current OS Prefers Dark Next Theme
system true light
system false dark
light dark
dark light

Applying a Theme

applyTheme(nextTheme, options?) performs three actions:

  1. Resolves whether the effective theme is dark: dark mode → dark, system mode → defers to prefersDark
  2. Toggles the CSS class: adds or removes the dark class on document.documentElement
  3. Persists the choice: writes the theme to localStorage under the key vlone-theme
ThemeManager.applyTheme('dark');
// → classList.add('dark'), localStorage.setItem('vlone-theme', 'dark')
// → returns 'dark'

ThemeManager.applyTheme('system');
// → resolves based on prefers-color-scheme
// → returns 'dark' or 'light' (the resolved visual theme)

Returns the resolved visual theme ('dark' or 'light') — useful for updating UI state after application.

SSR Safety

All browser APIs are guarded behind typeof checks:

const options = {
  documentRef: typeof document !== 'undefined' ? document : null,
  storage: typeof localStorage !== 'undefined' ? localStorage : null,
  prefersDark:
    typeof window !== 'undefined' ? (window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false) : false,
};

This makes applyTheme safe to call during server-side rendering — it becomes a no-op when browser globals are unavailable. Each dependency (document, localStorage, window.matchMedia) can also be injected via the options parameter for testing.

Reading Stored Theme

getStoredTheme(storage?) reads the persisted theme from localStorage:

const theme = ThemeManager.getStoredTheme();
// → 'light' | 'dark' | 'system'
// → defaults to 'system' if nothing is stored

The storage parameter can be injected for testing. If localStorage is unavailable (SSR), returns 'system'.

Persistence

Key Storage Default
vlone-theme localStorage 'system'

The key is defined in src/constants/keys.ts as Keys.Theme.

API Summary

import { ThemeManager, ThemeOptions, ThemeType } from '@/lib/internal/ThemeManager';

// Compute next theme for toggle button
const next = ThemeManager.computeNextTheme(current, prefersDark);

// Apply theme (resolves system, toggles class, persists)
const resolved = ThemeManager.applyTheme(next);
// resolved: 'dark' | 'light'

// Read persisted theme on page load
const stored = ThemeManager.getStoredTheme();

// Use in a dropdown
ThemeOptions.map(opt => <option value={opt.value}>{opt.label}</option>);

Design Decisions

  • Static class — no instantiation needed. All methods are stateless utilities that operate on injected or global references.
  • system as default — when no theme is stored, the CMS respects the user's OS preference rather than forcing a specific theme.
  • Single CSS class strategy — the dark class on <html> integrates with Tailwind's darkMode: 'class' configuration. Only one class is toggled; absence of dark implies light.
  • Toggle flips away from current visualcomputeNextTheme ensures one click always produces a visible change, even from system mode.
  • Dependency injectiondocument, localStorage, and prefersDark can all be overridden via options, enabling both SSR safety and deterministic testing.