theme-picker
Theme selection component for color mode (light/dark/auto), brand themes, and color accents.
Overview
The <theme-picker> component provides a UI for users to select their preferred color mode and brand theme. It integrates with ThemeManager for persistence and system preference detection.
<theme-picker> <button data-trigger> <icon-wc name="palette"></icon-wc> Theme </button></theme-picker>
Inline Variant
For settings pages, use the inline variant which is always visible:
<theme-picker variant="inline"></theme-picker>
Compact Mode
For high-theme-count scenarios (mobile menus, narrow sidebars), add compact to render the theme section as a grouped <select> dropdown instead of the swatch grid. This works with both popover and inline variants.
<theme-picker compact></theme-picker>
<theme-picker variant="inline" compact></theme-picker>
Trigger Variants
Text Button
Simple text trigger:
<theme-picker> <button data-trigger>Theme</button></theme-picker>
Icon-Only Button
For compact layouts, use an icon-only trigger with an accessible label:
<theme-picker> <button data-trigger aria-label="Theme settings"> <icon-wc name="palette"></icon-wc> </button></theme-picker>
Custom Styling
Apply button variant classes to match your design:
<theme-picker> <button data-trigger class="ghost"> <icon-wc name="palette" size="sm"></icon-wc> Theme </button></theme-picker>
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
variant |
popover, inline |
popover |
Display mode - popover with trigger or always visible |
compact |
boolean | - | Render theme section as grouped select dropdown instead of swatch grid |
open |
boolean | - | Reflected attribute indicating open state (popover only) |
ThemeManager API
For programmatic theme control, use the ThemeManager module:
import { ThemeManager } from './lib/theme-manager.js'; // Initialize (called automatically in main.js)ThemeManager.init(); // Get current stateconst { mode, brand, accent, effectiveMode } = ThemeManager.getState(); // Set color mode: 'auto', 'light', or 'dark'ThemeManager.setMode('dark'); // Set brand theme (personality themes)ThemeManager.setBrand('swiss'); // Set color accent: 'default', 'ocean', 'forest', 'sunset'ThemeManager.setAccent('ocean'); // Toggle between light and darkThemeManager.toggleMode(); // Reset to defaultsThemeManager.reset(); // Listen for changeswindow.addEventListener('vb:theme-change', (e) => { console.log(e.detail); // { mode, brand, accent, effectiveMode }});
Available Themes
Color Modes
| Mode | Description |
|---|---|
auto |
Follows system preference (prefers-color-scheme) |
light |
Forces light mode |
dark |
Forces dark mode |
Color Accents
Color accents are applied via ThemeManager.setAccent():
| Accent | Primary Hue | Description |
|---|---|---|
default |
260 (purple) | Default brand colors |
ocean |
200 (teal) | Cool blue-green palette |
forest |
145 (green) | Natural earthy greens |
sunset |
25 (orange) | Warm orange-red palette |
Creating Custom Themes
Create new brand themes by copying the template:
# Copy the templatecp src/tokens/themes/_brand-template.css src/tokens/themes/_brand-coral.css
Edit the hue values (0-360):
:root[data-theme="coral"],[data-theme="coral"] { --hue-primary: 15; /* coral orange */ --hue-secondary: 350; /* pink */ --hue-accent: 180; /* teal contrast */ /* Colors are auto-calculated from hues */ --color-primary: oklch(50% 0.15 var(--hue-primary)); /* ... */}
Register in src/tokens/themes/index.css:
@import "./_brand-coral.css";
Hue Reference
| Hue Range | Colors |
|---|---|
| 0-30 | Red / Orange |
| 30-60 | Orange / Yellow |
| 60-120 | Yellow / Green |
| 120-180 | Green / Cyan |
| 180-240 | Cyan / Blue |
| 240-300 | Blue / Purple |
| 300-360 | Purple / Red |
Preventing FOUC
To prevent a flash of unstyled content, add this inline script to your HTML <head>:
<script> try { const t = JSON.parse(localStorage.getItem('vb-theme') || '{}'); if (t.mode && t.mode !== 'auto') document.documentElement.dataset.mode = t.mode; if (t.brand && t.brand !== 'default') document.documentElement.dataset.theme = t.brand; } catch {}</script>
Events
| Event | Detail | Description |
|---|---|---|
theme-picker:open |
- | Fired when popover opens |
theme-picker:close |
- | Fired when popover closes |
vb:theme-change |
{ mode, brand, effectiveMode } |
Fired on window when theme changes |
Accessibility
- Popover uses
role="dialog"witharia-label - Radio groups use proper
role="radiogroup" - Trigger button announces expanded state via
aria-expanded - Escape key closes the popover
- Focus is trapped within popover when open