pfy.ch

My entire theme is driven of inline CSS variable definitions in my <html> element.

The HTML looks something like this:

<!DOCTYPE html>
<html
  lang="en"
  style="
    --colour-text: #000;
    --colour-background: #fff;
    --colour-background-sub: #eee;
  "
>
  <head>
    <title>Pfych</title>
  </head>
  <body>
    <h1 style="color: var(--colour-text)">Hello World</h1>
    <p class="example">Foo Bar Baz</p>
  </body>
</html>

CSS variables are accessible to any children elements.

I don’t use inline styles however, but CSS variables are still accessible if the component with the className is within the scope of the variable.

The following Stylesheet would apply the expected style in the example above

.example {
  color: var(--colour-text);
}

Knowing this we can manipulate the CSS variables with a function in our websites scripts:

const colours = {
  light: '#fff',
  dark: '#000',
};

export const setColourMode = () => {
  colourMode = getInitialColorMode() || 'light';
  const root = document.documentElement;

  root.style.setProperty(
    '--colour-text',
    colourMode === 'light' ? colours.dark : colours.light,
  );
};

The function getInitialColorMode() is based heavily on the React implementation by Josh Comeau.

export function getInitialColorMode() {
  const persistedColorPreference = window.localStorage.getItem('color-mode');
  const hasPersistedPreference = typeof persistedColorPreference === 'string';

  if (hasPersistedPreference) {
    return persistedColorPreference;
  }

  // Does the device have a prefered colour scheme?
  const mql = window.matchMedia('(prefers-color-scheme: dark)');
  const hasMediaQueryPreference = typeof mql.matches === 'boolean';
  if (hasMediaQueryPreference) {
    return mql.matches ? 'dark' : 'light';
  }

  return 'light';
}

we can then update the color-mode value in localstorage and call setColourMode() to toggle the theme.

To avoid flickering my root script.min.js is laid out in the following way:

setColourMode();

document.addEventListener('DOMContentLoaded', function () {
  // Other scripts here
}

This script is loaded first in the sites header, so it blocks any content from rendering. Nothing will render until setColourMode() has been run. All other functions are deferred until DOMContentloaded.

By using the colour CSS variables everywhere it’s possible to seamlessly swap themes across the site.


© 2024 Pfych