stellify/ui

Components

st-theme-switcher

A theme management component that handles light, dark, and system preferences with localStorage persistence and cross-instance synchronization.

Basic Usage

Wrap your theme toggle buttons with <st-theme-switcher>. Use data-theme attributes on child elements to define which theme each button activates.

<st-theme-switcher>
  <button type="button" data-theme="light">Light</button>
  <button type="button" data-theme="dark">Dark</button>
  <button type="button" data-theme="system">System</button>
</st-theme-switcher>

Icon Toggle Example

A common pattern is using icon buttons for the theme switcher:

<st-theme-switcher class="flex items-center gap-1">
  <button type="button" data-theme="light" class="p-2 rounded hover:bg-neutral-100 dark:hover:bg-neutral-800">
    <svg class="h-5 w-5"><!-- sun icon --></svg>
    <span class="sr-only">Light mode</span>
  </button>
  <button type="button" data-theme="dark" class="p-2 rounded hover:bg-neutral-100 dark:hover:bg-neutral-800">
    <svg class="h-5 w-5"><!-- moon icon --></svg>
    <span class="sr-only">Dark mode</span>
  </button>
  <button type="button" data-theme="system" class="p-2 rounded hover:bg-neutral-100 dark:hover:bg-neutral-800">
    <svg class="h-5 w-5"><!-- computer icon --></svg>
    <span class="sr-only">System preference</span>
  </button>
</st-theme-switcher>

How It Works

  • Clicking a button with data-theme="light|dark|system" updates the theme
  • Adds or removes the dark class on <html>
  • Persists the choice to localStorage under the key stellify.theme
  • Listens for system preference changes when set to "system"
  • Synchronizes across multiple <st-theme-switcher> instances on the same page

Theme Values

Value Behaviour
light Forces light mode (removes dark class)
dark Forces dark mode (adds dark class)
system Follows prefers-color-scheme media query

Properties

Property Type Description
theme "light" | "dark" | "system" Get or set the current theme preference
resolvedTheme "light" | "dark" The actual theme being applied (resolves "system" to light or dark)
const switcher = document.querySelector('st-theme-switcher')

// Get current preference
console.log(switcher.theme)         // "light" | "dark" | "system"

// Get resolved theme (what's actually applied)
console.log(switcher.resolvedTheme) // "light" | "dark"

// Set theme programmatically
switcher.theme = 'dark'

Events

Event Detail
st-theme-switcher:change { theme, resolvedTheme }
document.addEventListener('st-theme-switcher:change', (e) => {
  console.log('Theme changed:', e.detail.theme)
  console.log('Resolved to:', e.detail.resolvedTheme)
})

Active State Styling

The component automatically sets aria-current="true" on the button matching the current theme. Use this for styling the active state:

<style>
st-theme-switcher [data-theme][aria-current="true"] {
  background-color: var(--accent);
  color: var(--accent-foreground);
}
</style>

<!-- Or with Tailwind -->
<button data-theme="light" class="aria-[current=true]:bg-neutral-200 dark:aria-[current=true]:bg-neutral-700">
  Light
</button>

Tailwind CSS Integration

The component adds/removes the dark class on <html>, which works with Tailwind's class-based dark mode:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
}

Then use dark: variants throughout your markup:

<div class="bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white">
  Content adapts to theme
</div>

No-JavaScript Fallback

Without JavaScript, the theme defaults to system preference via CSS media queries. Consider adding a fallback script in your <head> to prevent flash of incorrect theme:

<script>
  (function() {
    var theme = localStorage.getItem('stellify.theme');
    if (theme === 'dark' || (theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  })();
</script>