Skip to content
Devix Open Source

Reference

Options

createThemeToggle(element, {
  themes: ['light', 'dark'],
  fallback: 'light',
  storageKey: 'theme',
  attribute: 'class',
  colorScheme: true,
  themeColor: undefined,
  disableTransitions: true,
  variant: 'segmented',
  system: true,
  labels: false,
});
Option Default
themes ['light','dark'] Any names you like — ['light','dark','sepia'].
fallback 'light' What system means when the machine says nothing.
dark names containing "dark" Which themes count as dark, for color-scheme.
storageKey 'theme'
remember true false keeps the choice for the page only.
attribute 'class' An attribute name, or a list of them.
element <html>
colorScheme true Sets color-scheme, so native controls follow too.
themeColor — { light: '#fff', dark: '#101014' } for the browser chrome.
disableTransitions true Stops the page animating every colour at once.
variant 'segmented' Or 'button', which cycles.
system true Offer system as a choice.
labels false Show the name beside the icon.
icons — Your own markup per theme.
theme — An existing controller to drive, instead of making one.

color-scheme is not optional

createThemeToggle(el);   // sets color-scheme by default

Without it, a dark page still has white scrollbars, a white caret, white <select> dropdowns and white date pickers. color-scheme is what tells the browser to render its own parts dark, and it is the single most-forgotten line in a dark theme.

theme-color is what people actually see on a phone

createThemeToggle(el, { themeColor: { light: '#ffffff', dark: '#101014' } });

On mobile this colours the browser's chrome around your page — the largest visible surface after the page itself. A dark site with a white bar above it looks broken.

If the page has no <meta name="theme-color">, one is created. If it has one, it is updated. Tags with a media attribute are left alone, because those are already doing this job.

Why transitions are suppressed

A stylesheet with transition: background .2s on many elements turns a theme switch into a two-hundred-millisecond smear of every colour on the page at once.

While switching, a transition: none rule is added, the change is made, a reflow is forced so the rule certainly applied, and the rule is removed. The theme changes instantly; everything else keeps its transitions.

disableTransitions: false if you have designed for the smear.

Named themes

createThemeToggle(el, {
  themes: ['light', 'dark', 'sepia', 'high-contrast'],
  attribute: 'data-theme',
  dark: ['dark', 'high-contrast'],
});

dark decides which of them get color-scheme: dark; by default it is any theme with "dark" in its name. Remember to pass the same themes to antiFlashScript(), or the snippet will not recognise a stored sepia and will fall back to the system.

Driving one controller from several places

import { createTheme, createThemeToggle } from '@devix-labs/theme-toggle';

const theme = createTheme({ themeColor: { light: '#fff', dark: '#101014' } });

createThemeToggle(header, { theme });
createThemeToggle(footer, { theme });

Both controls stay in step, and destroy() on either leaves the shared controller alone — only the one that created it disposes of it.

Updated 15 Sep 2026