Skip to content

ThemeToggle

import { ThemeToggle } from '@delightstack/components';

The live demos on this page are display-only: they cycle their icon and label but leave this site’s theme alone (each passes apply_theme={false}). Use the switch in the site header to change the theme.

View code
<ThemeToggle />

Bind to the theme prop to read or control the current theme preference.

<script>
import { ThemeToggle } from '@delightstack/components';
let theme = $state<'light' | 'dark' | 'auto'>('auto');
</script>
<ThemeToggle bind:theme />
<p>Current theme: {theme}</p>

Disable the system/auto option to cycle only between light and dark.

View code
<ThemeToggle disable_auto />

Set show_label to display the current mode name (“Light” / “Dark” / “Auto”) beside the icon. Pass label to override the text with your own.

View code
<ThemeToggle show_label />
<ThemeToggle show_label label="Appearance" />

Bind to is_dark to get the resolved dark state (taking system preference into account when theme is 'auto').

<script>
import { ThemeToggle } from '@delightstack/components';
let theme = $state<'light' | 'dark' | 'auto'>('auto');
let is_dark = $state(false);
</script>
<ThemeToggle bind:theme bind:is_dark />
{#if is_dark}
<p>Dark mode is active</p>
{:else}
<p>Light mode is active</p>
{/if}

Use a smaller size to fit in navigation bars.

<script>
import { ThemeToggle } from '@delightstack/components';
</script>
<header>
<nav><!-- nav items --></nav>
<ThemeToggle size="0" />
</header>

Use the onchange callback to run logic when the theme changes.

<ThemeToggle
onchange={(newTheme) => {
console.log('Theme changed to:', newTheme);
}}
/>

Override the default tooltip text.

View code
<ThemeToggle tooltip="Change appearance" />
PropTypeDefaultDescription
theme'light' | 'dark' | 'auto''auto'Current theme preference ($bindable())
is_darkbooleanfalseComputed resolved dark state ($bindable(), read-only). True when effective appearance is dark.
size'0' | '1' | '2''1'Component size ('0' = 18px icon, '1' = 24px icon, '2' = 32px icon)
disable_autobooleanfalseHide the auto/system option (two-state toggle only)
apply_themebooleantrueApply the theme to <html> as data-theme. Set false for a display-only toggle (a demo or preview): it still cycles, stores the choice and updates its own icon and label, but never touches <html>.
show_labelbooleanfalseShow a text label beside the icon (“Light” / “Dark” / “Auto”)
labelstringundefinedCustom label text override. Falls back to the current mode name when show_label is set.
tooltipstring''Override the default tooltip text. Defaults to the aria-label value.
idstringundefinedElement ID
classstring''Additional CSS classes
onchange(theme: 'light' | 'dark' | 'auto') => voidundefinedCalled when the theme changes
EventDetailDescription
onchange'light' | 'dark' | 'auto'Fires after cycling to a new theme state
ModeCycle
Three-state (default)light -> dark -> auto -> light
Two-state (disable_auto)light -> dark -> light

When the theme changes, the component:

  1. Sets data-theme="light" or data-theme="dark" on <html> for an explicit choice, and removes the attribute for 'auto' so the OS preference applies. @delightstack/styles maps the attribute to color-scheme. Skipped when apply_theme is false.
  2. Saves the preference to localStorage under the key 'delightstack:theme'.
  3. Watches for system preference changes via matchMedia('(prefers-color-scheme: dark)') and updates is_dark reactively when theme is 'auto'.

It does not set an inline color-scheme. A production build that lowers light-dark() (Lightning CSS, Vite’s default CSS minifier, does) replaces each call with custom properties that only the stylesheet’s color-scheme rules set, so an inline style would leave every token unchanged.

To prevent a flash of wrong theme on page load, add the following inline script to your HTML <head> (runs synchronously before the body renders):

<head>
<script>
try {
var saved = localStorage.getItem('delightstack:theme');
if (saved === 'light' || saved === 'dark') document.documentElement.dataset.theme = saved;
} catch (e) {}
</script>
</head>
  • Renders as a <button> element with a descriptive aria-label (e.g., “Theme: dark”, “Theme: auto (system preference)”)
  • Keyboard toggle via Space and Enter keys
  • Focus ring on :focus-visible
  • Tooltip via {@attach tooltip()} showing the current state
  • The SVG icon uses aria-hidden="true" since the button’s aria-label conveys the meaning
  • When theme is 'auto', a small “A” indicator appears on the button