Note: All controls on this page share its theme and use the
demo-only storage key theme-switcher-demo, except
the temporary control, which doesn’t save changes. Framed
examples have their own page themes and storage settings.
Use Tab to reach a group, arrow keys to move between choices, and Space to select. Only System follows changes to your device’s color preference.
The basics
Configure the control with attributes
The component sets data-theme on
<html>; the page’s CSS supplies the
colors. All markup is generated by the component.
<theme-switcher></theme-switcher>
/* Your page styles */
:root { color-scheme: light dark; }
:root[data-theme='light'] {
color-scheme: light;
}
:root[data-theme='dark'] {
color-scheme: dark;
}
body {
background: Canvas;
color: CanvasText;
}
Reload to try restoring the saved choice.
Make it yours
Example: Restyle the segments
Use the
base component’s
--multi-switch-* properties
to override the component’s default design.
<theme-switcher class="pill"
label="Site appearance">
</theme-switcher>
.pill {
--multi-switch-radius: 999px;
--multi-switch-border: #173f63;
--multi-switch-background: #eef4ff;
--multi-switch-color: #173f63;
--multi-switch-selected-background: #173f63;
--multi-switch-selected-color: #fff;
}
Add icons, with or without visible labels
This example uses CSS masks to add decorative icons. In
icon-only mode, it hides the labels
visually, but they remain available to assistive
technology. By default the
<legend> element is displayed, but
you can hide that using your own CSS.
<theme-switcher class="icons"
label="Icons and text">
</theme-switcher>
<theme-switcher class="icons hide-legend" icon-only
label="Icon-only appearance">
</theme-switcher>
.icons [data-multi-switch-choice]::before {
content: '';
inline-size: 1.25em;
block-size: 1.25em;
background: currentColor;
mask: var(--theme-icon) center / contain
no-repeat;
}
.icons [data-value='dark'] {
--theme-icon: url('./dark.svg');
}
/* Continues with system.svg and light.svg too. */
theme-switcher.hide-legend legend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Translate the labels
Default labels are English, but you can translate everything. Supply translated (or custom) legend and choice labels via their attributes.
<theme-switcher lang="fr"
label="Thème"
system-label="Appareil"
light-label="Clair"
dark-label="Sombre">
</theme-switcher>
<div lang="ar" dir="rtl">
<theme-switcher class="icons"
label="مظهر الموقع"
system-label="النظام"
light-label="فاتح"
dark-label="داكن">
</theme-switcher>
</div>
Want more themes?
You can add to the number of themes by replacing the choices. Be sure to include System, Light, and Dark alongside your custom options, then use your own CSS to implement the extra palettes.
<theme-switcher storage="none"
label="Reading palette"
choices='[
{"label":"System","value":"system"},
{"label":"Light","value":"light"},
{"label":"Dark","value":"dark"},
{"label":"Sepia","value":"sepia"}
]'>
</theme-switcher>
:root[data-theme='sepia'] {
color-scheme: light;
--page-background: #f6ecd6;
--page-color: #493921;
}
Storing preferences
By default, preferences are saved to `localStorage`, but you can control that and back it with cookies for server-side persistence.
Cookie example
Add the cookie attribute to save the user’s
preference in both `localStorage` and a cookie, so your
server can read it too. On startup, a valid
`localStorage` value wins, with the cookie as a
fallback. A bare attribute uses the storage key as the
cookie name; supply a value to name it separately.
storage="none" disables both stores.
<theme-switcher
storage-key="theme-switcher-cookie-demo"
cookie
label="Saved site appearance">
</theme-switcher>
<!-- Or choose a different cookie name: -->
<theme-switcher
storage-key="theme-switcher-cookie-demo"
cookie="server-theme">
</theme-switcher>
Plan for loading and failure
Keep a native fallback
If you want to enable a server-side fallback, you can do that by authoring a native fallback using a radio control group in your markup, wrapping it in this component. If the component fails to boot, the radios remain usable, and the form still has a value to submit.
Disable JavaScript and reload this page to compare authored controls with generated ones.
<theme-switcher>
<fieldset>
<legend>Color theme</legend>
<label><input type="radio"
name="theme" value="system"
checked>System</label>
<!-- Light and Dark radios too -->
</fieldset>
</theme-switcher>
/* No loading-state CSS hides the inputs.
No module means no page-theme behavior. */
Handle unavailable storage
A blocked storage API doesn’t stop the switcher from working. This isolated example simulates that restriction and displays the diagnostic information.
control.addEventListener(
'theme-switcher:error', ({ detail }) => {
console.error(detail.phase, detail.error);
});
// A storage error still allows:
await control.ready;
control.value = 'dark';
Apply a saved choice before first paint
Restore a saved preference before your stylesheets load to avoid a flash of the default theme. Match the storage key and allowed values to your control. Choose a theme in this frame, then use its reload link to try it. See the README for cookie restoration and Content Security Policy guidance.
<script>
try {
const theme = localStorage.getItem(
'theme-switcher-early-demo');
if (['system', 'light', 'dark']
.includes(theme)) {
document.documentElement
.setAttribute('data-theme', theme);
}
} catch (error) {
console.warn('Theme storage unavailable.',
error);
}
</script>
<link rel="stylesheet" href="./styles.css">
<!-- In the body: -->
<theme-switcher
storage-key="theme-switcher-early-demo"
label="Saved appearance">
</theme-switcher>