Note: This site is currently "Under construction". I'm migrating to a new version of my site building software. Lots of things are in a state of disrepair as a result (for example, footnote links aren't working). It's all part of the process of building in public. Most things should still be readable though.

A System Preferences Based Light/Dark Mode Switcher

Head's Up

This is a work in progress. I'm checking with folks for accessibility help and a smoke test.

Introduction

I'm building out an example site for my Neopoligen^neo^^ website builder. It'll act as the default "getting started" site. I'm putting in several things to make it easier to get up and running for folks who don't have experience with websites yet. One of those things is a light/dark mode switcher. This page is where I'm building and testing that functionality.

Goals

  • Make it a web component that can be included with a default set of components for the site

  • Make sure it's accessible

  • Start by reading the value from the system if one was defined

  • Show what the current system value is

  • Default to light mode if no system settings was detected

  • Provide the ability to manually toggle between dark and light mode

  • Provide the ability to fall back to using the system preferences (if they're available otherwise fallback to light mode)

  • Store your setting so it sets itself when you visit the site in another session

The Code

Here's what I've got so far. (I'm reaching out to some accessibility folks to see what else I need to do to make sure it's as accessible as possible)

HTML

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur venenatis, sem at laoreet facilisis, sapien nisi tincidunt purus, rhoncus lacinia lectus enim sit amet nibh. Nullam enim quam, ultricies et ipsum ut, porttitor laoreet turpis. Quisque eu massa.

<div class="example-wrapper">
  <example-color-switcher></example-color-switcher>
  <p>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur 
    venenatis, sem at laoreet facilisis, sapien nisi tincidunt purus, 
    rhoncus lacinia lectus enim sit amet nibh. Nullam enim quam, 
    ultricies et ipsum ut, porttitor laoreet turpis. Quisque eu massa.
  </p>
</div>

JavaScript

customElements.define('example-color-switcher', 
  class ExampleColorSwitcher extends HTMLElement {
    constructor() {
      super()
      this.attachShadow({ mode: 'open' })
      this.loadConfig()
      this.addStyles()
      this.addWrapper()
      this.addButtons()
      this.addListeners()
      this.setInitialMode()
    }

    addButtons() {
      for (let mode in this.config.modes) {
        const button = this.config.modes[mode]
        const btn =  this.ownerDocument.createElement('button')
        btn.dataset.mode = mode
        btn.setAttribute('role', 'exampleMode')
        btn.addEventListener('click', (event) => {
          this.handleClick.call(this, event)
        }) 
        if (button.mode !== 'auto') {
          btn.innerHTML = `${button.text} ${button.token}`
        }
        this.wrapper.appendChild(btn)
      }
    }

    addListeners() {
      window.matchMedia('(prefers-color-scheme: dark)')
        .addEventListener('change', () => {
          this.updateAutoDisplay.call(this)
        })
    }

    addStyles() {
      const styles =  this.ownerDocument.createElement('style')
      styles.innerHTML = `
[role="exampleMode"] {
  color: currentColor;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  outline: none;
  filter: brightness(60%);
  margin: 0;
  padding: 0;
}

[role="exampleMode"][aria-selected="true"] {
  border-bottom: 2px solid currentColor;
  filter: brightness(100%);
}

.switcher-wrapper {
  margin: 0;
  display: flex;
  flex-wrap: warp;
  gap: 1.4rem;
}
`
      this.shadowRoot.appendChild(styles)
    }

    addWrapper() {
      this.wrapper = this.ownerDocument.createElement('div')
      this.wrapper.classList.add('switcher-wrapper')
      this.shadowRoot.appendChild(this.wrapper)
    }

    handleClick(event) {
      this.setMode(event.target.dataset.mode)
    }

    loadConfig() {
      this.config = {
        modes: {
          light: { text: "Light", token: ""},
          dark: { text: "Dark", token: "" },
          auto: { text: "", token: "" },
        }
      }
    }

    setInitialMode() {
      this.updateAutoDisplay.call(this)
      const mode = localStorage.getItem('colorMode')
      if (mode) {
        this.setMode(mode)
      } else {
        this.setMode('auto')
      }
    }

    setMode(mode) {
      localStorage.setItem('colorMode', mode)
      if (mode === `auto`) {
        document.body.classList.remove('light')
        document.body.classList.remove('dark')
      } else {
        const removeMode = mode === 'light' ? 'dark' : 'light'
        document.body.classList.add(mode)
        document.body.classList.remove(removeMode)
      }
      const buttons = this.shadowRoot.querySelectorAll(`[role="exampleMode"]`)
      buttons.forEach((button) => {
        if (button.dataset.mode === mode) {
          button.setAttribute('aria-selected', true)
        } else {
          button.setAttribute('aria-selected', false)
        }
      })
    }

    updateAutoDisplay() {
      const els = this.shadowRoot.querySelectorAll('[role="exampleMode"][data-mode="auto"]')
      els.forEach((el) => {
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
          el.innerHTML = `Auto (${this.config.modes.dark.token})`
        } else {
          el.innerHTML = `Auto (${this.config.modes.light.token})`
        }
      })
    }
  }
)

CSS

body {
  --example-color: black;
  --example-bg-color: #ccc;
  --example-color-selected: black;
  --example-color-not-selected: #555;
}

body.dark {
  --example-color: #ccc;
  --example-bg-color: black;
  --example-color-selected: #ccc;
  --example-color-not-selected: #888;
}

@media (prefers-color-scheme: dark) { 
  body {
    --example-color: #ccc;
    --example-bg-color: black;
    --example-color-selected: #ccc;
    --example-color-not-selected: #888;
  }
  body.light {
    --example-color: black;
    --example-bg-color: #ccc;
    --example-color-selected: black;
    --example-color-not-selected: #555;
  }
}

.example-wrapper {
  color: var(--example-color);
  background-color: var(--example-bg-color);
}

example-color-switcher {
  display: inline-block;
  margin-block: 0.8rem;
}

Usage

I keep the javascript in a file called `components.js`` with the rest of my web components. That gets loaded on the page with:

Code

<script src="/path/to/components.js" type="module"></script>

The CSS resides in my base stylesheet.

Footnotes

  • (id:neo)
    the website building app I'm working on

References