Robert Birming

Bear Blog light / dark mode toggle

A light and dark mode toggle that lets visitors override their system preference, with the choice remembered on their next visit.1 2

It respects the visitor's system setting by default, and only takes over once they've actually clicked the toggle. No page reload, no flash of the wrong theme, just an instant switch between palettes.

Preview

toggle

A small icon button (sun in dark mode, moon in light mode) sitting in the nav. Clicking it flips the whole palette instantly, no page reload.

How to use

Markup

Add the button wherever you want the toggle to live, for example in your nav:

<button id="theme-toggle" aria-label="Toggle color theme">
  <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
    <circle cx="12" cy="12" r="4"/>
    <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
  </svg>
  <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
  </svg>
</button>

Script

In your Header directive, add this small script:

<script>
(function() {
  var t = localStorage.getItem('theme');
  if (t) document.documentElement.setAttribute('data-theme', t);
})();
</script>

In your Footer directive, add the toggle logic:

<script>
(function() {
  function setTheme(theme) {
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);
  }

  function currentTheme() {
    const attr = document.documentElement.getAttribute('data-theme');
    if (attr) return attr;
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }

  const toggle = document.getElementById('theme-toggle');
  if (toggle) {
    toggle.addEventListener('click', () => {
      setTheme(currentTheme() === 'dark' ? 'light' : 'dark');
    });
  }
})();
</script>

Styles

Add the following styles to your theme.

/* Toggle */
.icon-sun, .icon-moon {
  display: none;
  width: 1.1em;
  height: 1.1em;
  color: color-mix(in srgb, var(--text-color), transparent 25%);
  vertical-align: -0.15em;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .icon-moon {
    display: inline;
  }
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .icon-sun {
    display: inline;
  }
}

:root[data-theme="light"] .icon-moon {
  display: inline;
}

:root[data-theme="dark"] .icon-sun {
  display: inline;
}

#theme-toggle {
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  display: inline-flex;
  align-items: center;
}

@media (hover: hover) {
  #theme-toggle:hover {
    background: none;
    border-color: transparent;
  }
}

Updating your theme's colors

Your theme's :root block needs two small changes so the toggle has something to switch.

Here's a typical Bear-default color setup before the toggle is added:

:root {
  --background-color: #f8f8f8;
  --text-color: #2c2d2e;
  --link-color: #2952cc;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background-color: #01242e;
    --text-color: #d5d8dc;
    --link-color: #98cbe3;
  }
}

And here's the same setup after:

:root {
  --background-color: #f8f8f8;
  --text-color: #2c2d2e;
  --link-color: #2952cc;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --background-color: #01242e;
    --text-color: #d5d8dc;
    --link-color: #98cbe3;
  }
}

:root[data-theme="dark"] {
  --background-color: #01242e;
  --text-color: #d5d8dc;
  --link-color: #98cbe3;
}

:root[data-theme="light"] {
  --background-color: #f8f8f8;
  --text-color: #2c2d2e;
  --link-color: #2952cc;
}

Two changes, that's it:

  1. :not([data-theme]) added to the existing dark media query, so it only applies when the visitor hasn't clicked the toggle.
  2. Two new blocks at the bottom, [data-theme="dark"] and [data-theme="light"], repeating the same values. These are what the button actually switches between.

Apply the same pattern to the rest of your color tokens, --heading-color, --visited-color, --code-background-color, and so on, just repeat each one in both new blocks with its light and dark values.

Want more? Check out the full Bear Blog library.

  1. Requires JavaScript, available with Bear Blog subscription.

  2. Thanks to Yordi for the original tutorial on combining localStorage with light-dark(), and to Jedda and Chas for their own takes on it, which helped shape this one.