Theme Switcher Button

@use 'sprucecss/scss/spruce' as *;

.no-transition {
  * {
    transition: none !important;
  }
}

.theme-switcher {
  color: color('text');
  display: inline-flex;
  position: relative;

  &[data-theme-mode='system'] &__system-mode,
  &[data-theme-mode='light'] &__light-mode,
  &[data-theme-mode='dark'] &__dark-mode {
    display: inline-flex;
  }

  button {
    display: none;

    > * {
      pointer-events: none;
    }
  }
}
<div class="theme-switcher" data-theme-switcher>
    <button type="button" class="btn btn--outline-primary btn--sm btn--icon theme-switcher__system-mode" aria-label="Switch to light mode" data-action="light">
        <svg class="btn__icon" aria-hidden="true" focusable="false" height="24" style="fill:currentColor;overflow:visible;" viewBox="0 0 24 24" width="24" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
            <path d="M21,12c0,-4.969 -4.031,-9 -9,-9l0,18c4.969,0 9,-4.031 9,-9Zm3,0c0,6.628 -5.372,12 -12,12c-6.628,0 -12,-5.372 -12,-12c0,-6.628 5.372,-12 12,-12c6.628,0 12,5.372 12,12Z"></path>
        </svg>
    </button>
    <button type="button" class="btn btn--outline-primary btn--sm btn--icon theme-switcher__light-mode" aria-label="Switch to dark mode" data-action="dark">
        <svg class="btn__icon" aria-hidden="true" focusable="false" height="24" style="fill:currentColor;overflow:visible;" viewBox="0 0 24 24" width="24" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
            <path d="M5.509,3.917l-0.425,-0.426c-0.426,-0.425 -1.113,-0.415 -1.528,0l-0.01,0.011c-0.426,0.425 -0.426,1.113 -0,1.527l0.425,0.426c0.426,0.425 1.102,0.425 1.527,-0l0.011,-0.011c0.426,-0.415 0.426,-1.113 0,-1.527Zm-3.316,6.938l-1.113,-0c-0.6,-0 -1.08,0.48 -1.08,1.08l-0,0.011c-0,0.6 0.48,1.08 1.08,1.08l1.102,-0c0.611,0.011 1.091,-0.469 1.091,-1.069l-0,-0.011c-0,-0.611 -0.48,-1.091 -1.08,-1.091Zm9.818,-10.855l-0.011,-0c-0.61,-0 -1.09,0.48 -1.09,1.08l-0,1.047c-0,0.6 0.48,1.08 1.08,1.08l0.01,0c0.611,0.011 1.091,-0.469 1.091,-1.069l0,-1.058c0,-0.6 -0.48,-1.08 -1.08,-1.08Zm8.444,3.502c-0.425,-0.426 -1.112,-0.426 -1.538,-0.011l-0.425,0.426c-0.426,0.425 -0.426,1.112 -0,1.527l0.011,0.011c0.425,0.425 1.112,0.425 1.527,-0l0.425,-0.426c0.426,-0.425 0.426,-1.102 0,-1.527Zm-1.974,16.473l0.425,0.426c0.426,0.425 1.113,0.425 1.538,-0c0.426,-0.426 0.426,-1.113 0,-1.538l-0.425,-0.426c-0.426,-0.425 -1.113,-0.414 -1.527,0c-0.437,0.436 -0.437,1.113 -0.011,1.538Zm2.247,-8.04l0,0.011c0,0.6 0.48,1.08 1.08,1.08l1.102,-0c0.6,-0 1.08,-0.48 1.08,-1.08l-0,-0.011c-0,-0.6 -0.48,-1.08 -1.08,-1.08l-1.102,-0c-0.6,-0 -1.08,0.48 -1.08,1.08Zm-8.728,-6.535c-3.611,0 -6.545,2.935 -6.545,6.546c-0,3.611 2.934,6.546 6.545,6.546c3.612,-0 6.546,-2.935 6.546,-6.546c0,-3.611 -2.934,-6.546 -6.546,-6.546Zm-0.01,18.492l0.01,-0c0.6,-0 1.08,-0.48 1.08,-1.08l0,-1.048c0,-0.6 -0.48,-1.08 -1.08,-1.08l-0.01,0c-0.6,0 -1.08,0.48 -1.08,1.08l-0,1.048c-0,0.6 0.48,1.08 1.08,1.08Zm-8.444,-3.502c0.425,0.425 1.112,0.425 1.538,-0l0.425,-0.426c0.426,-0.425 0.415,-1.112 0,-1.527l-0.011,-0.011c-0.425,-0.425 -1.112,-0.425 -1.538,0l-0.425,0.426c-0.415,0.436 -0.415,1.112 0.011,1.538Z"></path>
        </svg>
    </button>
    <button type="button" class="btn btn--outline-primary btn--sm btn--icon theme-switcher__dark-mode" aria-label="Switch to system mode" data-action="system">
        <svg class="btn__icon" aria-hidden="true" focusable="false" height="24" style="fill:currentColor;overflow:visible;" viewBox="0 0 24 24" width="24" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
            <path d="M13.684,-0c-6.616,-0 -11.974,5.373 -11.974,12c0,6.627 5.358,12 11.974,12c3.246,0 6.187,-1.296 8.346,-3.396c0.268,-0.263 0.338,-0.67 0.166,-1.002c-0.171,-0.332 -0.541,-0.52 -0.911,-0.456c-0.525,0.092 -1.06,0.14 -1.612,0.14c-5.191,-0 -9.402,-4.222 -9.402,-9.429c0,-3.525 1.929,-6.595 4.784,-8.212c0.327,-0.188 0.493,-0.563 0.413,-0.927c-0.081,-0.364 -0.392,-0.638 -0.767,-0.67c-0.337,-0.027 -0.675,-0.043 -1.017,-0.043l-0,-0.005Z"></path>
        </svg>
    </button>
</div>

Theme Switcher Button

A one-button theme switcher that cycles through the system, light and dark modes, showing the icon of the current mode and saving the choice.

Anatomy

  • .theme-switcher with data-theme-switcher holds three buttons; only the one of the current mode (data-theme-mode on the switcher) is visible.
  • Each button's data-action is the mode it switches to: system → light → dark → system.

Accessibility

  • Each button's aria-label says what it does ("Switch to dark mode"), not what's active, so the action is clear. After a click, the focus moves to the newly visible button, which announces the next action.
  • The icons are decorative (aria-hidden="true").
  • If the three-state cycle is confusing for your audience, use the Theme Switcher Select, which lists the options.

Technical Details

  • Use it more than once on a page (in the header and the footer): every [data-theme-switcher] stays in sync.
  • It sets data-theme-mode on the html element, the default dark mode selector of Spruce (see the dark-mode settings), and stores the choice in localStorage (preferred-theme). In "system" mode, it follows the operating system live.
  • The no-transition class turns off the transitions for a frame, so the colors don't animate while switching.

Theme Detection

The switcher only changes the mode. To avoid a flash of the wrong colors, set the mode before the page renders with an inline script in the head:

<script>
  (() => {
    const preferred = localStorage.getItem('preferred-theme') ?? 'system';
    const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';

    document.documentElement.setAttribute('data-theme-mode', preferred === 'system' ? system : preferred);
  })();
</script>

With Spruce's dark-mode settings, prefers-color-scheme: true can follow the system without JavaScript; the switcher then stores an explicit choice. See Themes and Dark Mode.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.