Theme Switcher Select

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

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

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

  &[data-theme-mode='system'] .icon-system,
  &[data-theme-mode='light'] .icon-light,
  &[data-theme-mode='dark'] .icon-dark {
    display: inline-block;
  }

  &__icon {
    align-items: center;
    color: color('heading');
    display: flex;
    inline-size: 2em;
    inset-block: 0;
    inset-inline-start: 0;
    justify-content: center;
    pointer-events: none;
    position: absolute;

    svg {
      block-size: 0.85em;
      display: none;
      inline-size: 0.85em;
    }
  }

  &__select {
    padding-inline-start: 2em;
  }
}
<div class="theme-switcher" data-theme-switcher-select>
    <span class="theme-switcher__icon" aria-hidden="true">
        <svg class="icon-system" 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>
        <svg class="icon-light" 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>
        <svg class="icon-dark" 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>
    </span>
    <label class="sr-only" for="select-theme-mode">Color mode</label>
    <select class="form-control form-control--sm theme-switcher__select" id="select-theme-mode">
        <option value="system">System</option>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
    </select>
</div>

Theme Switcher Select

A theme switcher built on a native select: it lists the system, light and dark modes, shows the icon of the current one and saves the choice.

Anatomy

  • .theme-switcher with data-theme-switcher-select wraps an icon (__icon) and the select (__select).
  • The icon of the current mode is shown based on the switcher's data-theme-mode attribute.

Accessibility

  • A native select is the most robust control here: every option is named, the current one is announced, and it works the same with a keyboard, a screen reader and on touch screens.
  • The select has a (visually hidden) label ("Color mode"); the icon is decorative.

Technical Details

  • The script listens to change (not input), sets data-theme-mode on the html element and stores the choice in localStorage (preferred-theme). Several switchers on one page stay in sync, and "System" follows the operating system live.
  • The icon is positioned with inset-inline-start, so it moves to the right side in RTL along with the padding of the select.

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.