motion-theme-toggle
Light and dark mode control with an optional system option. The icon morphs from sun to moon on a spring. Add
wipe to reveal the new theme with a circular wipe that grows from the control. API-compatible with Chrome Labs'
dark-mode-toggle.
Preview
Each demo below themes its own card through target, so the rest of the site is left alone.
Three new components shipped, two bugs squashed, and the docs got a fresh coat of paint.
Import
import 'motion-components/motion-theme-toggle' // just this one
import 'motion-components' // or the whole libraryUsage
<motion-theme-toggle permanent></motion-theme-toggle>
<style>
:root[data-theme='light'] { --bg: #fff; --fg: #111; }
:root[data-theme='dark'] { --bg: #0b0b0f; --fg: #eee; }
body { background: var(--bg); color: var(--fg); }
</style>Icon with system
Add system to cycle light → dark → system. System follows the OS setting live.
<motion-theme-toggle system></motion-theme-toggle>Toggle
A segmented radio group, like the toggle appearance in Google Labs. The selection pill springs between options.
<motion-theme-toggle appearance="toggle" legend="Theme" system></motion-theme-toggle>Add icon-only to hide the labels. They stay available to screen readers, and each option shows its label as a tooltip.
<motion-theme-toggle appearance="toggle" icon-only system></motion-theme-toggle>Switch
A two-state switch with light and dark labels on either side. remember adds a checkbox bound to permanent.
<motion-theme-toggle appearance="switch" legend="Dark mode" remember="Remember"></motion-theme-toggle>Menu
A drop-down menu button. Opens with Enter, Space or ↓, and closes with Esc or an outside click.
<motion-theme-toggle appearance="menu" system></motion-theme-toggle>Page wipe
By default the theme changes at once. Add wipe to reveal the new theme with a circular view transition that grows from the
control. Use it where the theme change is the moment you want people to notice. For a site header, the default is usually calmer.
This demo targets <html>, so it switches the whole docs site. The theme menu in the site header is a
wipe. Toggles that share a target stay in
sync, so the header updates as well.
<motion-theme-toggle wipe></motion-theme-toggle>motion-theme-icon
The morphing glyph is its own primitive, <motion-theme-icon>, so you can use it as a theme indicator anywhere.
import 'motion-components/motion-theme-icon'
document.querySelector('motion-theme-icon').mode = 'dark'Preventing a flash on load
Components register after the page has painted. To apply the saved theme before the first paint, put this inline script in
<head>. It uses the same rules as the component: the saved choice first, then the OS setting.
<script>
(() => {
let mode = null
try { mode = localStorage.getItem('motion-theme') } catch {}
const os = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const scheme = mode === 'light' || mode === 'dark' ? mode : os
document.documentElement.dataset.theme = scheme
document.documentElement.style.colorScheme = scheme
})()
</script>Stylesheets per scheme
Stylesheets linked with a prefers-color-scheme media query are switched on and off to match the chosen theme,
so existing pages work unchanged:
<link rel="stylesheet" href="light.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">Properties
| Attribute | Property | Type | Default | Description |
|---|---|---|---|---|
| | | string | OS | |
| | | string | 'icon' | |
| | | boolean | false | Offers a third option that follows the OS preference. Ignored by |
| | | boolean | false | Saves the choice to localStorage and restores it on load. The key is |
| | | boolean | false | Reveals the new theme with a circular view-transition wipe from the control |
| | | boolean | false | Shows icons only in the |
| | | string | '' | Group label. Accessible name of the icon, switch and menu controls |
| | | string | 'Light' | Label of the light option |
| | | string | 'Dark' | Label of the dark option |
| | | string | 'System' | Label of the system option |
| | | string | '' | When set, shows a checkbox with this label that toggles |
| | | string | 'html' | CSS selector of the element that receives |
| | | number | 0.5 | Spring duration of the icon morph and wipe, in seconds |
| | | number | 0.25 | Spring bounciness of the icon morph and controls |
Icon properties
| Attribute | Property | Type | Default | Description |
|---|---|---|---|---|
| | | string | 'light' | |
| | | number | 0.5 | Spring duration of the morph, in seconds |
| | | number | 0.25 | Spring bounciness of the morph |
Events
| Event | Detail | Description |
|---|---|---|
| | | Fired when the mode or the applied scheme changes, including on first load. |
| | | Fired when |
document.addEventListener('colorschemechange', (e) => {
console.log(e.detail.colorScheme, e.detail.mode)
})CSS custom properties
| Property | Type | Default | Description |
|---|---|---|---|
| | color | currentColor 6% | Background of the track, segmented group and menu trigger |
| | color | currentColor 16% | Border color of the controls |
| | color | currentColor 14% | Selected segment, switch thumb and active menu item |
| | color | Canvas | Drop-down menu background |
| | length | 999px | Corner radius of the controls |
| | length | 1.25em | Icon size (1.5em on a standalone motion-theme-icon) |
Accessibility
icon and switch are buttons with role="switch" and aria-checked. With system,
the icon becomes a plain button whose label announces the current mode. toggle is a native radio group inside a
<fieldset>. menu follows the WAI-ARIA menu button pattern: aria-haspopup,
aria-expanded and menuitemradio items, with ↑ ↓ Home End to move,
Enter or Space to choose and Esc to close. With prefers-reduced-motion, the theme and icon
change instantly with no morph and no wipe.