ThemeToggle
A drop-in theme switcher — icon, switch, segmented, or menu — wired to ThemeProvider out of the box.
Each variant exposes a labelled, keyboard-operable control (button, switch, radio group, or menu). Decorative icons are aria-hidden; provide a label for icon-only variants.
ThemeToggle is a ready-made theme switcher composed from Cynosure components
(IconButton, Switch, ToggleGroup, DropdownMenu, and Tooltip). It reads
and writes the active theme through useTheme(), so
it must render inside a ThemeProvider — and the
selection persists through the provider’s storage like any other setTheme()
call.
import {
Card,
CardBody,
Stack,
Text,
ThemeProvider,
ThemeToggle,
useTheme,
} from '@arshad-shah/cynosure-react';
function Preview() {
const { theme, resolvedTheme } = useTheme();
return (
<Card variant="filled">
<CardBody>
<Stack gap="3" align="start">
<ThemeToggle />
<Text size="sm" color="fg.muted">
theme: <strong>{theme}</strong> · resolved: <strong>{resolvedTheme}</strong>
</Text>
</Stack>
</CardBody>
</Card>
);
}
export default function Example() {
return (
<ThemeProvider themes={['light', 'dark']} defaultTheme="system" storage={null}>
<Preview />
</ThemeProvider>
);
}
Variants
Section titled “Variants”Pick the variant that fits the surface:
icon(default) — a compact icon button that cycles throughmodeson click. Ideal for app headers.switch— a sun/moon switch for a binary light ↔ dark choice.segmented— an attached segmented control with one button per mode.menu— an icon button that opens a dropdown of mode options.
import { Inline, Stack, Text, ThemeProvider, ThemeToggle } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ThemeProvider themes={['light', 'dark']} defaultTheme="system" storage={null}>
<Stack gap="4">
<Inline gap="3" align="center">
<Text size="sm" width="120px" color="fg.muted">
icon
</Text>
<ThemeToggle variant="icon" />
</Inline>
<Inline gap="3" align="center">
<Text size="sm" width="120px" color="fg.muted">
switch
</Text>
<ThemeToggle variant="switch" />
</Inline>
<Inline gap="3" align="center">
<Text size="sm" width="120px" color="fg.muted">
segmented
</Text>
<ThemeToggle variant="segmented" />
</Inline>
<Inline gap="3" align="center">
<Text size="sm" width="120px" color="fg.muted">
menu
</Text>
<ThemeToggle variant="menu" />
</Inline>
</Stack>
</ThemeProvider>
);
}
Switch
Section titled “Switch”The switch variant is always binary (light ↔ dark) and reflects the resolved
colour scheme, so it shows “on” whenever the active theme renders dark — even
under system.
import { ThemeProvider, ThemeToggle } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ThemeProvider themes={['light', 'dark']} defaultTheme="light" storage={null}>
<ThemeToggle variant="switch" />
</ThemeProvider>
);
}
Segmented
Section titled “Segmented”The segmented control shows every mode at once. Set showLabels to render the
text label beside each icon.
import { Stack, ThemeProvider, ThemeToggle } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ThemeProvider themes={['light', 'dark']} defaultTheme="system" storage={null}>
<Stack gap="3" align="start">
<ThemeToggle variant="segmented" />
<ThemeToggle variant="segmented" showLabels />
</Stack>
</ThemeProvider>
);
}
The menu variant keeps a small footprint and lists the options in a
DropdownMenu radio group.
Three sizes — sm, md (default), and lg — scale the control and its icons.
import { Inline, ThemeProvider, ThemeToggle } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<ThemeProvider themes={['light', 'dark']} defaultTheme="system" storage={null}>
<Inline gap="4" align="center">
<ThemeToggle variant="segmented" size="sm" />
<ThemeToggle variant="segmented" size="md" />
<ThemeToggle variant="segmented" size="lg" />
</Inline>
</ThemeProvider>
);
}
Without the system option
Section titled “Without the system option”The default modes are ['light', 'dark', 'system']. If your ThemeProvider
sets enableSystem={false}, pass modes={['light', 'dark']} so the toggle
doesn’t offer a system choice that would fall back to the default theme.
import { Stack, ThemeProvider, ThemeToggle } from '@arshad-shah/cynosure-react';
export default function Example() {
// `enableSystem={false}` removes the system option; mirror it on the toggle
// by limiting `modes` to light + dark.
return (
<ThemeProvider
themes={['light', 'dark']}
defaultTheme="light"
enableSystem={false}
storage={null}
>
<Stack gap="3" align="start">
<ThemeToggle variant="segmented" modes={['light', 'dark']} showLabels />
<ThemeToggle variant="icon" modes={['light', 'dark']} />
</Stack>
</ThemeProvider>
);
}
Customising icons and labels
Section titled “Customising icons and labels”Override the per-mode icons and labels to match your brand or copy:
import { MonitorSmartphone, MoonStar, SunMedium } from 'lucide-react';
<ThemeToggle variant="menu" icons={{ light: <SunMedium size={18} />, dark: <MoonStar size={18} />, system: <MonitorSmartphone size={18} /> }} labels={{ system: 'Match device' }}/>;Flash-free on first paint
Section titled “Flash-free on first paint”ThemeToggle only changes the theme at runtime. To avoid a light-then-dark
flash on reload, render the init script from getThemeInitScript() in your
document <head> as described in Dark mode. The
toggle then reflects whatever the script resolved.
Accessibility
Section titled “Accessibility”- Every variant renders a labelled, keyboard-operable control: the
iconandmenuvariants use anIconButtonwhoselabelbecomes thearia-label;segmentedis arole="radiogroup"with arrow-key navigation;switchis arole="switch". - Mode icons are decorative (
aria-hidden). For icon-only variants the accessible name comes fromlabel(icon/menu) or each item’s mode label (segmented). - The
iconvariant pairs with aTooltipthat names the current theme. - Because the control mutates the document theme, keep a single primary
ThemeToggleper view so screen-reader users aren’t presented with competing controls.
Recipes
Section titled “Recipes”- Reach for
variant="icon"in a dense top bar; it cycles light → dark → system. - Use
variant="segmented"withshowLabelsin a settings panel where all options should be visible. - Use
variant="menu"when header space is tight but you still want explicit options. - Drop
'system'frommodeswhenever the provider hasenableSystem={false}.