Mosmox UI
Current version
GitHubnpmjs.com
Navigation & Selection

Theme Switcher

A localized light, dark, and system theme control with persistence, token palettes, and accessible controls.

ThemeSwitcher gives users a consistent way to choose between light, dark, and system color modes. It can be rendered as a segmented control or a compact cycling button.

The component is designed to work with Mosmox UI tokens. By default it applies its palette to the nearest ConfigProvider scope, so the rest of the UI can consume the updated CSS variables without every component receiving a theme prop.

Installation

import { ConfigProvider, ThemeSwitcher } from '@mosmox/ui';

Basic usage

<ConfigProvider>
  <ThemeSwitcher />
</ConfigProvider>

The default segmented control provides Light, Dark, and System options. System follows prefers-color-scheme and updates when the operating system preference changes.

Display variants

Use segmented when all modes should be visible, or button for a compact control that cycles through the configured options:

<ThemeSwitcher variant="segmented" />
<ThemeSwitcher variant="button" showLabel={false} />

options controls which modes are available and their order. It accepts any non-empty subset of light, dark, and system; the button variant cycles in that order, while the segmented variant renders one button per option.

<ThemeSwitcher options={['light', 'dark']} />
<ThemeSwitcher variant="button" options={['system', 'dark']} />

Sizes

ThemeSwitcher supports three control sizes: xs for dense toolbars, sm for compact controls, and default for regular layouts.

<ThemeSwitcher size="xs" />
<ThemeSwitcher size="sm" />
<ThemeSwitcher size="default" />

The showLabel prop hides visible text while preserving accessible names and icons, so it can be combined with any size when space is limited.

Controlled state and persistence

Use value and onChange when the application owns the theme state:

const [theme, setTheme] = useState<ThemeSwitcherMode>('system');

<ThemeSwitcher value={theme} onChange={setTheme} />

Without value, the component manages its own state. It stores the selected mode under mosmox-theme by default. Set a custom storageKey, or set persist={false} to disable local storage and cross-tab synchronization.

<ThemeSwitcher storageKey="account-theme" />
<ThemeSwitcher persist={false} defaultValue="dark" />

Token palettes

The switcher applies built-in light and dark token palettes to the nearest .mm-config-provider. Override only the values needed by the application with lightTokens or darkTokens:

<ThemeSwitcher
  darkTokens={{
    primary: '#8b5cf6',
    primaryHover: '#a78bfa',
    controlSelected: '#34265e',
  }}
/>

Set applyTheme={false} when the application handles CSS variables itself and the component should only expose the selected mode:

<ThemeSwitcher applyTheme={false} onChange={(mode) => document.documentElement.dataset.theme = mode} />

When applyTheme is enabled, the component applies the resolved light/dark palette to the nearest .mm-config-provider; without a provider it uses the document root. The system mode resolves to the current prefers-color-scheme value and reacts when that media preference changes. lightTokens override the built-in light palette and darkTokens override the built-in dark palette only for their corresponding resolved theme.

Supported token names include surface, surfaceHover, text, mutedText, primary, primaryHover, secondary, secondaryHover, controlBorder, controlFilled, controlSelected, controlIcon, lightBackground, lightHover, placeholder, focus, and overlay.

Localization

Built-in labels are available for English (en), Ukrainian (uk), German (de), and French (fr). The component uses the nearest ConfigProvider locale:

<ConfigProvider locale="uk">
  <ThemeSwitcher variant="segmented" />
</ConfigProvider>

Use locale to override the provider for one instance, or supply individual labels:

<ThemeSwitcher
  locale="en"
  labels={{
    label: 'Appearance',
    light: 'Day',
    dark: 'Night',
    system: 'Device',
  }}
/>

Default mode labels are:

LocaleLightDarkSystem
enLightDarkSystem
ukСвітлаТемнаСистемна
deHellDunkelSystem
frClairSombreSystème

labels is merged over the localized defaults, so you can override only one label or provide all application-specific labels. label controls the accessible group name; light, dark, and system control the visible mode text.

Accessibility

The segmented and button variants use native buttons with aria-pressed. Labels remain available to assistive technology when showLabel={false}, and the wrapper receives an accessible aria-label by default.

Theme changes also set data-mm-theme and color-scheme on the active scope, allowing application CSS to react to the resolved light or dark mode.

Set disabled to prevent both direct selection and button cycling. className, style, and other standard HTMLAttributes<HTMLDivElement> are forwarded to the root element; the component removes only the native onChange attribute because it exposes its own typed callback.

API reference

PropertyTypeDefaultDescription
valuelight | dark | system—Controlled theme mode.
defaultValuelight | dark | systemsystemInitial mode for uncontrolled usage.
onChange(mode) => void—Called after the selected mode changes.
storageKeystringmosmox-themeLocal storage key used when persistence is enabled.
persistbooleantruePersists the mode and listens for changes from other tabs.
applyThemebooleantrueApplies the resolved token palette and scope attributes.
optionsThemeSwitcherMode[]['light', 'dark', 'system']Modes shown by the control.
labelsThemeSwitcherLabelsprovider localeOverrides localized labels.
localeen | uk | de | frprovider localeSelects the component language.
lightTokensThemeSwitcherTokensbuilt-in light paletteOverrides light mode tokens.
darkTokensThemeSwitcherTokensbuilt-in dark paletteOverrides dark mode tokens.
showLabelbooleantrueShows visible mode labels.
sizexs | sm | defaultdefaultControl size: extra small, small, or regular.
variantbutton | segmentedsegmentedControl presentation.
disabledbooleanfalseDisables theme changes.

ConfigProvider defaults

The provider can define the default presentation for all nested theme switchers:

<ConfigProvider
  components={{
    themeSwitcher: {
      size: 'sm',
      variant: 'button',
      showLabel: false,
      radius: '999px',
    },
  }}
>
  <ThemeSwitcher />
</ConfigProvider>

Per-instance props take precedence over ConfigProvider.components.themeSwitcher defaults. The provider supplies size, variant, showLabel, and radius; locale is inherited separately and can be overridden with the component-level locale prop.