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:
systemis the default, so first-time visitors get their device theme and keep getting it when the device switches (for example at sunset).- Choosing
systemagain clears the stored choice. Onlylightanddarkare saved. - `ThemeToggle` cycles device, light, dark and shows a monitor icon while following the device. So do the toggles in
AppBarandPageLayout. toggleTheme()takes the same cycle. To pick a mode directly, for example from a menu, callsetThemeMode('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
defaultThemelater reaches everyone who never picked a theme. - Combine it with
allowSystemto offer all three modes while starting fromlightordark. Choosingsystemis 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
- `ThemeProvider` takes
storageKey,favicon,faviconTitle,allowSystemanddefaultTheme. - `useTheme()` returns
{ activeTheme, themeMode, setThemeMode, allowSystem, toggleTheme }. - `ThemeToggle` is a ready-made button for
toggleTheme. - `useAppTheme` is the hook behind the provider, for use without a context.
- `buildFaviconHref(theme)` builds a theme-colored favicon data URL.
- `getThemeInitScript()` returns an inline script that sets the theme before the first paint.
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.