Example Components
Theme Toggle
A button that switches between light and dark themes and remembers the choice, plus theme-init, which applies the saved choice before the first paint. Without a saved choice, the operating system setting applies.
Usage
Put <theme-init /> in the <head> of every page, usually through a shared head component, and <theme-toggle /> wherever the button belongs. Both need the color-scheme rules from a token stylesheet; see Theming.
true
examples/components/theme-toggle/theme-toggle.html
html
<theme-toggle /> html examples/components/theme-toggle/theme-toggle.html
<!-- @bascik
A button that switches between light and dark themes and remembers the choice. Needs <theme-init /> in <head> and the color-scheme rules in styles.css. Hidden until its script runs.
-->
<style>
.toggle {
display: inline-flex;
align-items: center;
gap: var(--space-2, 0.5rem);
padding: var(--space-1, 0.25rem) var(--space-2, 0.5rem);
border: 1px solid var(--color-border, currentColor);
border-radius: var(--radius, 0.5rem);
background: var(--color-surface, color-mix(in srgb, currentColor 8%, transparent));
color: inherit;
font: inherit;
cursor: pointer;
}
</style>
<button class="toggle" id="toggle" type="button" hidden>Dark theme</button>
<script>
// aria-pressed tells assistive technology whether dark theme is on; the label stays the same.
const toggle = document.getElementById('toggle');
const media = matchMedia('(prefers-color-scheme: dark)');
const preview = toggle.closest('[data-demo-theme]');
const themeTarget = preview ?? document.documentElement;
const themeAttribute = preview ? 'data-demo-theme' : 'data-theme';
const storageKey = preview ? 'theme-demo' : 'theme';
if (preview) {
try {
const savedTheme = localStorage.getItem(storageKey);
if (savedTheme === 'light' || savedTheme === 'dark') preview.setAttribute(themeAttribute, savedTheme);
} catch {
// The preview keeps its default theme when storage is unavailable.
}
}
const current = () => themeTarget.getAttribute(themeAttribute) ?? (media.matches ? 'dark' : 'light');
const render = () => {
toggle.setAttribute('aria-pressed', String(current() === 'dark'));
};
toggle.addEventListener('click', () => {
const next = current() === 'dark' ? 'light' : 'dark';
themeTarget.setAttribute(themeAttribute, next);
try {
localStorage.setItem(storageKey, next);
} catch {
// Not persisted when storage is unavailable; the choice still applies here.
}
render();
// Other components can react without reaching into this one.
toggle.dispatchEvent(new CustomEvent('theme-change', { bubbles: true, detail: { theme: next } }));
});
media.addEventListener('change', render);
// Stay in sync when another toggle, or any other script, changes the theme.
new MutationObserver(render).observe(themeTarget, { attributes: true, attributeFilter: [themeAttribute] });
render();
toggle.hidden = false;
</script> Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
The button above switches only the preview theme. It stores the choice under theme-demo, separate from the docs site's theme setting.
theme-init
html examples/components/theme-init/theme-init.html
<!-- @bascik
Applies the saved light or dark theme before first paint. Place it in <head> of every page so there is no flash of the wrong theme; its script stays in <head> and runs before the body renders. Pairs with <theme-toggle />.
-->
<!-- Tells the browser both schemes are supported before any CSS loads, so form controls and the canvas match. -->
<meta name="color-scheme" content="light dark" />
<script>
try {
const theme = localStorage.getItem('theme');
if (theme === 'light' || theme === 'dark') document.documentElement.setAttribute('data-theme', theme);
} catch {
// Storage can be unavailable (private mode, blocked cookies). The operating system theme then applies.
}
</script> How It Works
theme-initruns synchronously in<head>, before the body renders, and setsdata-themeon<html>fromlocalStorage. Bascik keeps a component's script in<head>when the component is used there, so there is no flash of the wrong theme.- On a normal site, the token stylesheet reads
data-themefrom<html>, so switching the attribute recolors the whole page. In this preview, the toggle instead changes the preview'sdata-demo-themeand stores the choice undertheme-demo. - The button exposes its state with
aria-pressedand ishiddenuntil its script runs. - After each change it dispatches a bubbling
theme-changeevent, so other components can react without reaching into this one.
Adapt It
- For a three-way system, light, and dark choice, use a
<select>and remove the stored value for "system". See Theming.