ThemeSwitcher
A button that switches the theme between light, dark, and system, and remembers the choice in localStorage. It cycles through the three by default, or toggles between light and dark only.
Playground
Variant
Size
Strategy
·
<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
An 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.
·
<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 theme follows the OS through color-scheme: light dark, so prefers-color-scheme changes take effect on their own.
04 API Reference
11 props11 props
Add filter
Sort
Grouping · No column can be grouped
Summary · No column can be summarized
Column visibility
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' | Button size. | |
slotClasses | Partial<Record<ThemeSwitcherSlots, string>> | — | Per-slot class overrides. | |
storageKey | false | 'urbicon-theme' | localStorage key holding the choice, or false to switch persistence
off. A stored value is read once on mount; 'system' is persisted as the
ABSENCE of the key, so the OS keeps winning after a reload. Where storage
is unusable nothing is written and nothing throws — the theme is back to
'system' on the next load. | |
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>
(() => {
var d = document.documentElement;
// Storage is not guaranteed usable: reading it throws where it is switched
// off (a hardened profile, an embedded webview), and an unguarded read
// aborts the rest of this head script.
var read = (k) => {
try {
return localStorage.getItem(k);
} catch {
return null;
}
};
// Only explicit choices set a class; system mode leaves
// color-scheme: light dark to follow the OS via light-dark().
var t = read('urbicon-theme');
if (t === 'dark') d.classList.add('dark');
else if (t === 'light') d.classList.add('light');
})();
</script>