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.

Weekly digest

Three new components shipped, two bugs squashed, and the docs got a fresh coat of paint.

Components Release

Import

import 'motion-components/motion-theme-toggle'   // just this one
import 'motion-components'                       // or the whole library

Usage

<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.

system
<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.

Appearance
<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 <motion-theme-toggle appearance="menu" system permanent> without wipe. Toggles that share a target stay in sync, so the header updates as well.

Click to switch the docs theme with a wipe
<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
mode mode string OS 'light', 'dark' or 'system': the selected mode. Reflects to the attribute. 'system' needs the system attribute
appearance appearance string 'icon' 'icon', 'toggle', 'switch' or 'menu': the visual style of the control
system system boolean false Offers a third option that follows the OS preference. Ignored by switch
permanent permanent boolean false Saves the choice to localStorage and restores it on load. The key is motion-theme for <html>, and motion-theme:<target> for any other target
wipe wipe boolean false Reveals the new theme with a circular view-transition wipe from the control
icon-only iconOnly boolean false Shows icons only in the toggle appearance. Labels stay available to screen readers and as tooltips
legend legend string '' Group label. Accessible name of the icon, switch and menu controls
light light string 'Light' Label of the light option
dark dark string 'Dark' Label of the dark option
system-label systemLabel string 'System' Label of the system option
remember remember string '' When set, shows a checkbox with this label that toggles permanent
target target string 'html' CSS selector of the element that receives data-theme and color-scheme
duration duration number 0.5 Spring duration of the icon morph and wipe, in seconds
bounce bounce number 0.25 Spring bounciness of the icon morph and controls

Icon properties

Attribute Property Type Default Description
mode mode string 'light' 'light' (sun), 'dark' (moon) or 'system' (half disc): the glyph to show
duration duration number 0.5 Spring duration of the morph, in seconds
bounce bounce number 0.25 Spring bounciness of the morph

Events

Event Detail Description
colorschemechange colorSchememode Fired when the mode or the applied scheme changes, including on first load.
permanentcolorschemechange permanent Fired when permanent changes.
document.addEventListener('colorschemechange', (e) => {
  console.log(e.detail.colorScheme, e.detail.mode)
})

CSS custom properties

Property Type Default Description
--mc-theme-toggle-surface color currentColor 6% Background of the track, segmented group and menu trigger
--mc-theme-toggle-border color currentColor 16% Border color of the controls
--mc-theme-toggle-accent color currentColor 14% Selected segment, switch thumb and active menu item
--mc-theme-toggle-menu-bg color Canvas Drop-down menu background
--mc-theme-toggle-radius length 999px Corner radius of the controls
--mc-theme-icon-size 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.