Have you ever been on your laptop, late in the evening and just thought, “My eyes sure are tired”? Speaking for myself, this happens quite a bit and I find that the bright white background of most sites tends to be the main cause.

This isn’t just a musing or my own idea, there are studies that show bright whites/bright screens lead to faster eye fatigue. So, it’s no wonder when an app offers a dark theme or a night mode that I tend to enable this feature right away.

As a web developer myself, building and enabling features like this have often meant writing multiple styles and sending down multiple CSS files for both light and dark mode. However, all of this has changed with the introduction of System Wide Dark Mode and even more recently with the introduction of the prefers-color-scheme media query.

Hello Darkness My Old Friend…

First, it probably helps to know what exactly “Dark Mode” is when it comes to user interfaces before we dive into more information.

Dark Mode is a way of inverting colors so that UI that was light becomes dark and vise versa. Got black text on a white background? With dark mode, this switches to white text on a dark background. While this feature may sound simple, in reality, there are many pieces that must be taken into consideration to build it. Parts of an App (or OS even) will need to be adjusted to make sure they are still usable once dark mode is applied.

Mac System apps dark mode animation

Although the ability to apply dark mode has been around for a while, it typically has only been accessible to native developers or locked down as a system feature. Trying to add these features to the web has lead to some less than ideal approaches like:

  • Shipping two CSS files for light and dark
  • Dynamically downloading the different CSS for light and dark

Both of these approaches often involve complex build systems and different theming with SCSS Variables being used. Luckily, there are new alternatives that have emerged to make it easier to apply dark mode to the web.

Beacons of Light In Our Dark Times

While older approaches of extra CSS may have worked in the past, we now have a new way of handling dynamic CSS. CSS Variables allow us to declare custom values in our styles, which can be updated at runtime. For instance, say we have the following CSS:

div {
  --backgroundVar: #000000;
  background: var(--backgroundVar);
}

By using --backgroundVar as the value of background, we can set the background color to black. Now, any time we want to modify the background of our element, we only need to target the CSS variable of --backgroundVar.

div.light-background {
  --backgroundVar: #ffffff;
}

This is how theming is done in Ionic, meaning we can completely change the overall look of our app all at runtime. Nontheless, this requires different classes to be applied and for the users to make the switch. But, what if we want to use System Dark Mode?

To target the system setting, we have a new media query available, prefers-color-scheme. This media query allow developers to hook into a user theme preference and provide the correct styles.

html {
  background: #ffffff;
  color: #000000;
}

@media (prefers-color-scheme: dark) {
  html {
    background: #000000;
    color: #ffffff;
  }
}

We can then take this to its next logical step and combine both CSS Variables and the prefers-color-scheme media query to simplify our styles.

// Applied accorss the entire app
:root {
  --bgColor: #ffffff;
  --textColor: #000000;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bgColor: #000000;
    --textColor: #ffffff;
  }
}


html {
  background: var(--bgColor);
  color: var(--textColor);
}

While perhaps a bit contrived, it’s easy to imagine how this could scale across an entire app. In fact, we have this snippet for adding dark mode to an entire Ionic app (test it out!):

@media (prefers-color-scheme: dark) {
  :root {

    --ion-border-color: var(--ion-color-dark-shade);
    --ion-background-color: var(--ion-color-dark);
    --ion-background-color-rgb: var(--ion-color-dark-rgb);
    --ion-text-color: var(--ion-color-light);
    --ion-text-color-rgb: var(--ion-color-light-rgb);

    --ion-color-step-50: #232323;
    --ion-color-step-100: #2e2e2e;
    --ion-color-step-150: #3a3a3a;
    --ion-color-step-200: #454545;
    --ion-color-step-250: #515151;
    --ion-color-step-300: #5d5d5d;
    --ion-color-step-350: #8b8b8b;
    --ion-color-step-400: #747474;
    --ion-color-step-450: #7f7f7f;
    --ion-color-step-500: #8b8b8b;
    --ion-color-step-550: #979797;
    --ion-color-step-600: #a2a2a2;
    --ion-color-step-650: #aeaeae;
    --ion-color-step-700: #b9b9b9;
    --ion-color-step-750: #c5c5c5;
    --ion-color-step-800: #d1d1d1;
    --ion-color-step-850: #dcdcdc;
    --ion-color-step-900: #e8e8e8;
    --ion-color-step-950: #f3f3f3;
  }
}

To see it live, visit Star Track in Safari with System Dark Mode enabled.

Star Track animations

Dark Side of the Moon

While dark mode is super awesome, we do need to let the reality sink in a bit. Currently, dark mode is only available in Desktop Safari and Firefox. Meaning that while we may want to darken all the things, we still need to provide a fallback experience for the majority of users. This could be through setting a class on the body element and targeting that with our CSS or loading additional CSS as we’ve stated before.

Thankfully, with Ionic’s CSS variables, we get a nice “happy path” to take. We can use the media query for browsers that support it, and fall back to a class selector for those that don’t. Even if we use the class-selector approach, we still have far less CSS being sent down to the user thanks to CSS Variables.

While still a bit early, having official support for dark mode in a few browsers, some shortcuts, or a mechanism to at least hook into user settings is a huge step forward to making your app much easier on the eyes and improving usability for your audience.