CSS Custom Properties

I’ve been leaning on CSS custom properties more and more at work. They’ve been around for years now, and I think they’re a little underused and underappreciated. Unlike other newish features in CSS, like grid, they don’t really unlock new ways of styling a page that weren’t possible before. Instead, they’re a great organisational tool for the programmer, which is powerful in its own way.

It’s almost too obvious to point out, but the ability to give things semantic labels is important; and it’s a feature that was missing from stylesheets until relatively recently. You can emulate it in various ways with preprocessors and css-in-js, but having the feature in the native platform seriously reduces the attraction of tools like SCSS or Emotion.

Probably the most well-known application of custom properties is in theming. Define a set of colour variables, use them in place of colour literals, then redefine them to change the theme. That’s what I did for this website, whose base stylesheet looks a bit like this:

:root {
  --neutral-hue: 328;

  --neutral-light: hsl(var(--neutral-hue), 56%, 95%);
  --background-light: #orange;
  --selection-light: #e91e63;

  --neutral-dark: hsl(var(--neutral-hue), 63%, 25%);
  --background-dark: #033162;
  --selection-dark: #e91e63;

  --neutral: var(--neutral-light);
  --background: var(--background-light);
  --selection: var(--selection-light);
}

@media (prefers-color-scheme: dark) {
  :root {
    --neutral: var(--neutral-dark);
    --background: var(--background-dark);
    --selection: var(--selection-dark);
  }
}

An important point about custom properties is that they’re composable: a variable can be defined in terms of other variables. In the example above this allows us to separate the definition of the colours (--background-light etc) from the question of which theme we’re using (--background: var(--background-dark)).

Another nice thing is that, unlike SCSS variables, they’re defined at runtime and are re-definable. I decided it’d be nice if the colour theme of this site changes slowly over the course of the year, so I broke out --neutral-hue as its own variable, and added the following script to the top of the page:

<script>
  const date = new Date();
  const today = Math.floor(
    (date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24
  );
  const percentage = today / 365;
  const hue = Math.floor(360 * percentage);
  document.querySelector(":root").style.setProperty("--neutral-hue", `${hue}`);
</script>

So there’s a backup hue defined in the CSS file, which will be used if the user doesn’t have JavaScript enabled, but in roughly 100% of cases it’ll be overridden by the same property set in a style attribute. It sometimes looks a little odd, especially in dark mode, but overall I like the effect.

There’s something very satisfying about the encapsulation this approach to dynamic styling provides. The “logic” of styling is defined entirely in the CSS; the JavaScript is only responsible for changing the value of a single variable. One of the worst things that can happen when working with, say, a css-in-js library is that you end up with almost all of the styling defined by piles of interpolated strings, and this pattern is a nice guard against that.

Note: Usually it’s best practice to add a type="module" or type="defer" attribute to scripts, to stop them blocking the page from rendering while they’re evaluated, but in this case blocking is actually the correct behaviour. The script is minuscule, and if the page is allowed to render before it runs, there’ll be a brief paint of the default colour, leading to flashing. No good.