ThemeSwitcher
Light/dark/system theme switcher with localStorage persistence, system preference detection, and multiple interaction modes.
Playground
<ThemeSwitcher />01 Examples
Cycle vs. toggle
Default cycles light → dark → system. Pass strategy='toggle' to flip between light and dark only.
<ThemeSwitcher />
<ThemeSwitcher strategy="toggle" />Variants and sizes
<ThemeSwitcher variant="ghost" size="sm" />
<ThemeSwitcher variant="outlined" size="md" />
<ThemeSwitcher variant="filled" size="lg" />In a settings panel
Realistic use — outlined trigger inline with related appearance preferences.
Appearance
Color theme
Reduce motion
<div
class="bg-surface-elevated border-border-subtle w-full overflow-hidden rounded-2xl border"
>
<div class="border-border-subtle border-b px-5 py-3">
<h3 class="text-text-primary text-sm font-semibold">Appearance</h3>
</div>
<div class="divide-border-subtle divide-y">
<div class="flex items-center justify-between px-5 py-3">
<p class="text-text-primary text-sm font-medium">Color theme</p>
<ThemeSwitcher variant="outlined" size="sm" />
</div>
<div class="flex items-center justify-between px-5 py-3">
<p class="text-text-primary text-sm font-medium">Reduce motion</p>
<Toggle size="sm" intent="primary" />
</div>
</div>
</div>02 Customization
Branded trigger
Override the button and icon slots for a gradient brand-look. Pass storageKey=false for ephemeral switching without persistence.
<ThemeSwitcher
slotClasses={{
button:
'bg-linear-to-r from-violet-500 to-fuchsia-500 text-white hover:from-violet-600 hover:to-fuchsia-600 shadow-md shadow-violet-500/20',
icon: 'h-5 w-5'
}}
/>03 Accessibility
The trigger carries a dynamic aria-label and title that name the active theme ("Light mode" / "Dark
mode" / "System theme"). Focusable via Tab, activated with Enter / Space; uses focus-visible: for keyboard-only rings. In system mode
the UI live-follows prefers-color-scheme changes natively via color-scheme: light dark — no JavaScript needed.
04 API Reference
11 props
Prop | Type | Default | Description | |
|---|---|---|---|---|
class | string | — | Additional CSS classes. | |
disabled | boolean | — | Disable the switcher. | |
onThemeChange | (theme: Theme) => void | — | Called after the theme changes. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ ThemeSwitcher: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
size | ThemeSwitcherVariants['size'] | 'md' | Size variant that controls dimensions and spacing of the ThemeSwitcher | |
slotClasses | Partial<Record<ThemeSwitcherSlots, string>> | — | Per-slot class overrides. | |
storageKey | false | 'urbicon-theme' | localStorage key for persistence. Set to false to disable. | |
strategy | cycletoggle | 'cycle' | Interaction mode.
- 'cycle' — single button cycling light → dark → system (default)
- 'toggle' — single button toggling light ↔ dark (no system option) | |
theme | Theme | 'system' | Current theme. Supports bind:theme. | |
unstyled | boolean | false | Strip all default styles. | |
variant | ThemeSwitcherVariants['variant'] | 'ghost' | Visual style of the button. |
05 Types
Local type definitions used by this component.
6 types
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
Theme | type | helper | 1 | — | |
ThemeSwitcherProps | interface | props | 0 | Props for the ThemeSwitcher component. | |
ThemeSwitcherSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
ThemeSwitcherVariants | type | variant | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 0 | — |
06 Installation
Import
import { ThemeSwitcher } from '@urbicon-ui/blocks';FOUC Prevention
<!-- Add to app.html <head> for flash-free theme loading -->
<script>
// Only explicit choices set a class; system mode leaves
// color-scheme: light dark to follow the OS via light-dark().
const t = localStorage.getItem('urbicon-theme');
if (t === 'dark') document.documentElement.classList.add('dark');
else if (t === 'light') document.documentElement.classList.add('light');
</script>