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 current = () => document.documentElement.getAttribute('data-theme') ?? (media.matches ? 'dark' : 'light');
const render = () => {
toggle.setAttribute('aria-pressed', String(current() === 'dark'));
};
toggle.addEventListener('click', () => {
const next = current() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try {
localStorage.setItem('theme', next);
} catch {
// Not persisted when storage is unavailable; the choice still applies to this page.
}
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(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
render();
toggle.hidden = false;
</script> Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
The button above switches this page's theme. It stores the choice in localStorage, which this docs site does not read on the next visit.
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.- The token stylesheet writes every color as
light-dark(light, dark)and setscolor-schemefromdata-theme, so switching the attribute recolors the whole page with no other rules. - 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.