Theme Toggle
A three-way segment for appearance — Device, Light, and Dark.
Device follows prefers-color-scheme; Light / Dark force the scheme by
flipping data-mode so Pho’s light-dark() tokens resolve correctly.
Import
import { ThemeToggle } from "@photon-ai/pho-ui/components/theme-toggle";
Basic
Icon-only segments in a hairline track. The selected option is the inverse
chip (same language as Tabs); unselected segments carry no fill — hover only
raises the icon ink. Default preference is device.
<ThemeToggle />
Sizes
Four sizes — xs, sm, md (default), lg. Use xs in dense menus.
<ThemeToggle size="xs" />
<ThemeToggle size="sm" />
<ThemeToggle />
<ThemeToggle size="lg" />
Controlled
Drive mode / onModeChange when the parent owns appearance state. Pass a
target to scope the resolved scheme to a subtree — useful in previews.
Appearance
Preference: device. Scoped to this preview card.
const [mode, setMode] = useState<"device" | "light" | "dark">("device");
<ThemeToggle mode={mode} onModeChange={setMode} />;
Avoiding a flash
Persist under localStorage (pho-mode by default). Add a blocking script in
<head> so the first paint already matches the stored choice:
<script>
const mode = localStorage.getItem("pho-mode");
const prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
if (mode === "dark" || ((mode === "device" || mode == null) && prefersDark))
document.documentElement.dataset.mode = "dark";
</script>
Props
- mode — controlled
"device"·"light"·"dark" - defaultMode — uncontrolled initial preference (default
device) - onModeChange —
(mode) => void, fired when a segment is chosen - storageKey —
localStoragekey, ornullto skip persistence (default"pho-mode") - target — element (or getter) that receives
data-mode(default<html>) - size —
xs·sm·md·lg(defaultmd) - aria-label — name for the radiogroup (default
"Theme") - className — merged onto the track
Also exports getThemePreference, getThemeMode, setThemeMode,
resolveThemeMode, parseThemeMode, and THEME_STORAGE_KEY.
Accessibility
- Root is a
radiogroup; each segment is aradiowith anaria-label(Device/Light/Dark) andaria-checkedfor the selection. - Roving tabindex — only the selected segment is tabbable; arrow keys move across the group (Home / End jump to the ends).
- Focus uses the brand ring (
focus-visibleonly).
Keyboard:
- Tab — focus the selected segment
- ← / → (or ↑ / ↓) — move selection
- Home / End — first / last option
- Space / Enter — activate the focused segment