Skip to content

ThemeToggle

A drop-in theme switcher — icon, switch, segmented, or menu — wired to ThemeProvider out of the box.

  • stable
  • since v3.4.0
  • theme
  • dark-mode
  • control
  • toggle

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.

Preview
Open
tsx

Pick the variant that fits the surface:

  • icon (default) — a compact icon button that cycles through modes on 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.
Preview
Open
tsx

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.

Preview
Open
tsx

The segmented control shows every mode at once. Set showLabels to render the text label beside each icon.

Preview
Open
tsx

The menu variant keeps a small footprint and lists the options in a DropdownMenu radio group.

Preview
Open
tsx

Three sizes — sm, md (default), and lg — scale the control and its icons.

Preview
Open
tsx

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.

Preview
Open
tsx

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' }}
/>;

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.

PropTypeDefaultDescription
variant
"icon"|"switch"|"segmented"|"menu"
icon
Presentation. One of `icon`, `switch`, `segmented`, `menu`.
size
"sm"|"md"|"lg"
md
Control scale. One of `sm`, `md`, `lg`.
modes
ThemeMode[]
['light', 'dark', 'system']
Modes to offer, in order. Drives the `icon` cycle order, the `segmented` buttons, and the `menu` items. Drop `'system'` if your `ThemeProvider` sets `enableSystem={false}`. The `switch` variant is always binary (light↔dark) and ignores this list.
labels
Partial<Record<ThemeMode, string>>
Override the visible/accessible label for one or more modes.
icons
Partial<Record<ThemeMode, ReactNode>>
Override the icon for one or more modes.
showLabels
boolean
false
Show the text label next to the icon in the `segmented` variant (the `menu` variant always shows labels; `icon` never does).
label
string
Toggle theme
Accessible name for the control (and heading text for the `menu` variant).
className
string
Additional class names on the root element.
  • Every variant renders a labelled, keyboard-operable control: the icon and menu variants use an IconButton whose label becomes the aria-label; segmented is a role="radiogroup" with arrow-key navigation; switch is a role="switch".
  • Mode icons are decorative (aria-hidden). For icon-only variants the accessible name comes from label (icon/menu) or each item’s mode label (segmented).
  • The icon variant pairs with a Tooltip that names the current theme.
  • Because the control mutates the document theme, keep a single primary ThemeToggle per view so screen-reader users aren’t presented with competing controls.
  • Reach for variant="icon" in a dense top bar; it cycles light → dark → system.
  • Use variant="segmented" with showLabels in 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' from modes whenever the provider has enableSystem={false}.