Ryan and I started working on dark mode support for jirametrics and have discovered that CSS now has much better support for this sort of thing than it’s ever had in the past. Specifically, the latest versions of all the main browsers, now support dark mode directly, as well as CSS variables, which simplifies much of the configuration.

Let’s take a look at how that all works.

The first piece to the puzzle is support for dark mode media queries. This CSS will set the background to white by default and will change that to the darker #bbb when dark mode is enabled on the machine.

body {
  background: white;
}

@media screen and (prefers-color-scheme: dark) {
  body {
    background: #bbb;
  }
}

Note that I could have gone the other way and made dark the default and then used prefers-color-scheme: light to override that.

It’s important to call out that these styles are dynamic. If you change your machine back and forth between light and dark mode, you will see these changes happening in real-time, without having to reload the page.

To make the CSS more maintainable, we also use variables which are fairly new to CSS. In the past, we would have used a preprocessor like SASS but today, variables are natively supported by the browsers.

:root {
  --body-background: white;
  --non-working-days-color: #F0F0F0;
  --expedited-color: red;
  --blocked-color: #FF7400;
  --stalled-color: orange;

}
@media screen and (prefers-color-scheme: dark) {
  :root {
    --body-background: #222;
    --non-working-days-color: black;
  }
}

CSS variables all start with the double dash and are scoped. In this case, we’re scoping them all to the root but we could have made them finer grained than that.

As before, we’re overriding some of the variables when in dark mode.

Then when we want to use one of those values in our CSS, we access it through var() and there are two flavours of this; one that just takes the variable name var(--body-background) and one that takes the variable name and a default value var(--body-background, red).

:root {
  --body-background: white;
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --body-background: #bbb;
  }
}
body {
  background: var(--body-background);
}

This is good enough for static content but we use a lot of javascript charts and so we need javascript to be able to access these same variables. From javascript, we can retrieve the value of a variable like this:

getComputedStyle(document.body).getPropertyValue('--body-background')

The gotcha with pulling these values through Javascript is that you’re getting the current value and are not automatically getting the updated value when the user switches between light and dark modes. So you need to have some way to know when the switch happened so that you can reload the new values through Javascript again.

This listener will tell you when a switch has happened, and which way it switched in case you need that.

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
  if (event.matches) {
    // now in dark mode
  } else {
    // now in light mode
  }
})

In our case, we have to force a rebuild of all our charts when this happens so right now, we’re taking the easy way out and forcing a reload of the page. It’s a little ugly but gets the desired result.

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
  location.reload()
})

Unfinished business: What we’ve done above is perfect for most of the report but we still have one chart we haven’t figured out how to get working with light/dark mode. Our dependency chart relies on graphviz to generate an SVG diagram of nodes and links. This is generated at the time we create the report and we haven’t found a way to pass in CSS variable names. We may have to look at different tools for this, that will generate the SVG in the browser instead.