Theme switch

A theme switch button component for manually switching color-schemes whilst respecting user preferences for light or dark mode viewing, and optional switch button as used on this website.

Requires $enable-theme-switch: true; in the configuration.scss document, the script theme.js [assets/js] loaded prior to the style sheets in the head of the HTML document (to negate FOUC when changing or refreshing pages), and a button with the same attributes as below needs to be included somewhere on the page.

Script

The script has been adapted from the one used in Adam Argyle's Building a theme switch component article to use an inline style on the <html> attribute.

const storageKey = 'theme-preference'

const onClick = () => {
  theme.value = theme.value === 'light'
  ? 'dark'
  : 'light'
  setPreference()
}

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
  return localStorage.getItem(storageKey)
  else
  return window.matchMedia('(prefers-color-scheme: dark)').matches
  ? 'dark'
  : 'light'
}

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

const reflectPreference = () => {
  document.firstElementChild
  .style.setProperty('color-scheme', theme.value)
  
  document.firstElementChild
  .setAttribute('class', theme.value)
  
  document
  .querySelector('#themes')
  ?.setAttribute('aria-label', theme.value)
}

const theme = {
  value: getColorPreference(),
}

reflectPreference()

window.onload = () => {
  reflectPreference()
  document
  .querySelector('#themes')
  .addEventListener('click', onClick)
}

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
  theme.value = isDark ? 'true' : 'false'
  setPreference()
})
<button id="themes" aria-label="light">Theme</button>

The script alters the aria-label attribute depending on the current theme, and adds inline styling on the page <html> attribute to apply the alternative color-schemes. The button is provided unstyled so can be customized as required or the optional switch button can be used as described below.

Switch button (anchor)

The optional switch button provides a sun and moon icon switch button as used in the site's navigation, it requires both $enable-switch-button: true; and $enable-theme-switch: true; in the configuration.scss document. The icon styles have been written to work independently if the individual icons or utility styles are not also enabled.

<button id="themes" aria-label="light"><span class="vis-hidden">theme</span></button>

The styles are provided in the _theme-switch.scss document in the [styles/components] directory, the optional class utility .vis-hidden can be enabled with $enable-vis-hidden: true in the configuration.scss document.

_theme-switch.scss

Currently the styles still require $enable-forms-buttons: true; for the basic button styles but the icon styling will still work with fallback user-agent styles so can be customized as required if the button styles are disabled.

//  ------------------------------------------------------------
//  Theme switch button
//  ------------------------------------------------------------
@use "../../configuration" as *;
@use "../../properties" as *;
@use "icons" as *;

@if $enable-theme-switch {

@if $enable-sun-svg and $enable-moon-svg {
  :where(html) {
    --switch-off: var(--sun);
    --switch-on: var(--moon);
  }
}
@else {
  :where(html) {
    --switch-off: #{$sun};
    --switch-on: #{$moon};
  }
}

#themes {
  --ico: var(--color);
  --svg: var(--switch-ico);
  --btn-px: .5rem;
  --ico-va: -.12em;
}
  
#themes:before {
  @extend %icon-mask;
}

:where(html:is(.light)) #themes {
  --svg: var(--switch-off);
}

:where(html:is(.dark)) #themes {
  --svg: var(--switch-on);
}

} // END [if/theme-switch]

The script also includes the alternating .light and .dark classes with the <html> attribute that are used to apply the individual icons with CSS :is() styles as demonstrated above. This method stops the icons from a small FOUC that happens if the icon styles were applied using the alternating [aria-label] attribute.