Skip to main content

Theme switcher

The Theme Switcher lets users toggle between light, dark, and system themes, with the latter adapting to the device's preference. It's powered by two JavaScript scripts—one in the head section and one at the bottom of the page—to ensure smooth transitions. Several variants cater to different UI needs.

Default

Theme
<fieldset>
      <legend>Theme</legend>
      <div class="bg">
          <div class="form-check form-check-inline">
              <input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="auto" aria-label="System" id="theme-system">
              <label for="theme-system" class="form-check-label">System</label>
          </div>
          <div class="form-check form-check-inline">
              <input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="light" aria-label="Light" id="theme-light">
              <label for="theme-light" class="form-check-label">Light</label>
          </div> 
          <div class="form-check form-check-inline">
              <input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="dark" aria-label="Dark" id="theme-dark">
              <label for="theme-dark" class="form-check-label">Dark</label>
          </div>
      </div>
  </fieldset>

It is advisable to add id attributes to radio buttons and corresponding for attributes to labels. This enhancement allows users to click the label as well as the radio button itself, improving accessibility and usability. However, the code functions independently of these id attributes, permitting the addition of multiple switchers to the page as needed.

Hamburger menu

This variant is designed to sit on a blue background and can be used in a site's context or hamburger menu. It merely changes the colour of the title to white

Theme
<form class="theme-switch hamburger-menu">
...
</form>

Compact

In this variant, all the options presented slightly smaller and inline. A thin divider is present at the top of the switcher

Theme
<form class="theme-switch theme-compact">
...
</form>

Right nav

When using the theme switcher underneath the right hand nav, simply add the class theme-right-nav to get the appropriate amount of right margin. You can see this in action underneath the nav panel.

<form class="theme-switch theme-compact theme-right-nav">
...
</form>

Javascript

The first script is placed in the <head> to ensure the theme is set as early as possible, reducing the flash of unstyled content (FOUC). It determines the user's preferred theme (either from local storage or system settings) and applies it immediately.

<script>
  (function() {
    'use strict';
const getStoredTheme = () => localStorage.getItem('theme');

const getPreferredTheme = () => {
  const storedTheme = getStoredTheme();
  return storedTheme ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
};

const setTheme = theme => {
  const themeToSet = theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
  document.documentElement.setAttribute('data-bs-theme', themeToSet);
};

setTheme(getPreferredTheme()); 

})(); </script>

The second script, placed at the bottom of the page, handles the theme switching functionality for the website. It retrieves the user's preferred theme from local storage or system settings and applies it to the document. It also provides the ability to switch themes through a theme switcher UI component, updates the UI to reflect the active theme, and listens for changes to the system's dark mode preference.

This script uses data-bs-theme-value to drive the logic, rather than ids, to allow multiple instances of the switcher to be present on a single page. This is unlikely, but may occur, a switcher in the hamburger menu and a compact version at the bottom of the page.

<script>
  (function() {
    'use strict';
const getStoredTheme = () => localStorage.getItem('theme');

const setStoredTheme = theme => localStorage.setItem('theme', theme);

const getPreferredTheme = () => {
  const storedTheme = getStoredTheme();
  return storedTheme ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
};

const setTheme = theme => {
  const themeToSet = theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
  document.documentElement.setAttribute('data-bs-theme', themeToSet);
};

const showActiveTheme = theme => {
  document.querySelectorAll('.theme-switch').forEach(themeSwitcher => {
    themeSwitcher.querySelectorAll('[data-bs-theme-value]').forEach(element => {
      element.checked = (element.getAttribute('data-bs-theme-value') === theme);
    });
  });
};

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  const storedTheme = getStoredTheme();
  if (storedTheme !== 'light' && storedTheme !== 'dark') {
    setTheme(getPreferredTheme());
  }
});

window.addEventListener('DOMContentLoaded', () => {
  showActiveTheme(getPreferredTheme());
  document.querySelectorAll('.theme-switch [data-bs-theme-value]').forEach(toggle => {
    toggle.addEventListener('change', () => {
      const theme = toggle.getAttribute('data-bs-theme-value');
      setStoredTheme(theme);
      setTheme(theme);
      showActiveTheme(theme);
    });
  });
});

})(); </script>