Theme

Light, dark and the device theme

The theme is driven by <html data-theme="light|dark">. Wrap your app in ThemeProvider to set it. The user can choose a fixed theme, and the choice is remembered under storageKey.

<ThemeProvider storageKey="my-app-theme" favicon>
	<App />
</ThemeProvider>

Following the device theme

By default the app follows the device theme until the user toggles; from then on their choice is stored. Add allowSystem to make "follow the device" a real third choice that stays selectable and is the default for new visitors.

<ThemeProvider storageKey="my-app-theme" allowSystem>
	<App />
</ThemeProvider>
  • light: always the light theme.
  • dark: always the dark theme.
  • system: follow the device (prefers-color-scheme), including live changes while the page is open.

Mode: system. On screen: light.

With allowSystem:

  • system is the default, so first-time visitors get their device theme and keep getting it when the device switches (for example at sunset).
  • Choosing system again clears the stored choice. Only light and dark are saved.
  • `ThemeToggle` cycles device, light, dark and shows a monitor icon while following the device. So do the toggles in AppBar and PageLayout.
  • toggleTheme() takes the same cycle. To pick a mode directly, for example from a menu, call setThemeMode('system' | 'light' | 'dark').

Without allowSystem nothing changes: toggleTheme() switches between light and dark.

import { useTheme } from 'xedonium'

function ThemeMenu() {
	const { themeMode, setThemeMode, activeTheme } = useTheme()
	// themeMode: the choice ('system' | 'light' | 'dark'); activeTheme: what is on screen ('light' | 'dark')
	return (
		<select value={themeMode} onChange={e => setThemeMode(e.target.value)}>
			<option value="system">Device ({activeTheme})</option>
			<option value="light">Light</option>
			<option value="dark">Dark</option>
		</select>
	)
}

Default theme

defaultTheme (light, dark or system, default system) is the theme used until the user chooses one. Use it for an app that should open in light, or in dark, regardless of the device.

<ThemeProvider storageKey="my-app-theme" defaultTheme="light">
	<App />
</ThemeProvider>
  • The default is not stored. Only a different choice is saved, and choosing the default again clears it, so changing defaultTheme later reaches everyone who never picked a theme.
  • Combine it with allowSystem to offer all three modes while starting from light or dark. Choosing system is then saved, so it sticks.
  • A saved choice always wins over the default.

Avoiding a flash

ThemeProvider sets data-theme once the app has hydrated, so server-rendered pages can show the wrong theme for a moment. `getThemeInitScript` returns a small inline script that applies the saved choice, or the default, before the first paint. Pass it the same storageKey and defaultTheme as the provider.

import { getThemeInitScript } from 'xedonium'

// pages/_document.tsx (Next.js)
<script
	dangerouslySetInnerHTML={{ __html: getThemeInitScript({ storageKey: 'my-app-theme', defaultTheme: 'light' }) }}
/>

Switching

Changing the theme crossfades the whole page using the browser's View Transitions API. Browsers without it, and users who prefer reduced motion, get the plain colour transitions instead.

Reference

All of them are documented, with examples, in the Theme section.

Tailwind's dark: variant is wired to the same attribute by the preset (darkMode: ['selector', '[data-theme="dark"]']). data-theme always holds the resolved theme (light or dark), never system, so your own CSS only needs to handle those two.