Hesoyam

Theme Switcher

Light, System, and Dark in one control. It reads and writes the shared geist-theme store.

The live control below changes the docs theme. forced-theme is a locked preview and does not write storage.

Default

Three labeled options. The thumb tracks the current theme from useTheme.

Theme system · resolved dark


        
1 <ThemeSwitcher />

Small

Pass small for footers and menus. Keep the default size on a settings page.


        
1 <ThemeSwitcher small />

Disabled

disabled is for a read-only preview of the control. Prefer forced-theme when the app itself locks the palette.


        
1 <ThemeSwitcher disabled />

Forced theme

forced-theme locks the thumb on light or dark and disables the group. Use it when the surrounding surface cannot follow System.


        
1 <ThemeSwitcher forced-theme="dark" />
2 <ThemeSwitcher forced-theme="light" />

useTheme

Call setTheme from settings or a menu. Do not mirror the value into a second local ref.

initTheme runs once on the client, reads localStorage key geist-theme, and toggles .dark on documentElement. system follows prefers-color-scheme.


        
1 import { ThemeSwitcher, useTheme } from '@/components/ui/theme-switcher'
2
3 const { theme, resolvedTheme, setTheme } = useTheme()
4
5 setTheme('dark')

Best practices

  • One Theme Switcher per app — footer or settings. Do not drop a second copy on every page.
  • Use this control for Light / System / Dark. Do not rebuild it with Switch or three icon buttons.
  • small for dense chrome. Default size when the labels have room to breathe.
  • Read and write through useTheme. Mirroring the value into local page state will desync the thumb.
  • forced-theme is the lock when a surface cannot follow System. disabled is only for a static preview of the control.
  • Leave the Light / System / Dark labels alone. They already have per-option aria-labels.