Switch to Dark Mode

What I need is an easy way to apply Dark Mode properly, for my blog and for the projects I'm working on. I like Dark Mode and I want my clients and their customers to experience it too. But for clients, the effort to add Dark Mode is difficult to justify. Which is why it should be easy for developers to build and add. Unfortunately, making good tools is usually not that easy.

Example Light vs Dark Mode

The Importance of Dark Mode

Some believe Dark Mode is overrated: a lot of effort for so little. It could even cause more damage than good. It is important that your website listens to the user's settings. And yes, there are more important features. However, this is a Whataboutism argument and not a proper objection. It betrays someone's own preference. Don't take Dark Mode off your wishing list just yet. Dark Mode provides 30% less power consumption on OLED screens and it is pleasant for eyes in the evening or dark environments, because your eyes do not have to adjust to the light as often. In addition, it is more pleasant for users who are hypersensitive to light.

Applying Dark Mode also forced me to simplify my color system and embed contrast values. In the past I checked afterwards if there was enough contrast and fixed it; mind-numbing with a high risk of errors. From now on, I set the hierarchy of colors from the start. I converted my personal blog, simple in structure and which I hadn't worked on for years, to Dark Mode in an hour.

Dark Mode is not just a setting

Users will set knowingly Light or Dark Mode. For settings generaly spoken, once something is a setting, only a group of experienced users will find and use it. But Dark Mode is not just a setting. With every new iPhone and Mac or an OS update, the system will ask if you want to use Light Mode, Dark Mode or an automatic time switch. This is enough reason to take Dark Mode seriously. Unfortunately, I have not yet been able to find good statistics about usage. There are, however, hints that Dark Mode is really liked. Also about 80% in our organization raises their hand when I ask who uses Dark Mode. This does not mean that you should only offer your website in Dark Mode. Again, let your website listen to user preferences. There are those who prefer Light Mode, such as those who simply like black on white or users who suffer from Astigmatism and for whom Dark Mode is annoying. Astigmatism is caused by irregular curvature of the cornea or deviations in the lens of the eye, so that the light is not properly distributed on the retina. You will see a 'glow' around light text on a dark background. The Astigmatism effect is more severe with less light. Most people have more or less Astigmatism, but that doesn't mean they won't use Dark Mode. I also suffer somewhat from Astigmatism and still prefer Dark Mode, but only if it is implemented correctly.

No one-way hierarchy for shades

Every project has a shade hierarchy for each color: from light to dark or from dark to light, depending on how you look at it. That works great for Light Mode, but not for Dark Mode. It is not a one-way street. In Dark Mode it has to get darker before it gets lighter. In addition, the color names (names of the variables) for Light and Dark Mode must be the same, so you can adjust the 'content' of the variables in one central place in your CSS (root) and you do not have to do that for each element. With has several consequences. For example, you cannot use consecutive numbers in color names, because that is confusing. What is darker and lighter compared to the background color? What has sufficient contrast? The name of the color variant must be descriptive, each with its own purpose. Long story short, a well-functioning hierarchy is: Root, Ground, Low, Moderate, High, Important and Critical. Borrowed mostly from Microsoft's severity rating.

In the following examples I use the neutral color Base. You can also use the rules for other colors: hierarchy colors (Primary, Secondary, Tertiary, Quaternary), status colors (Success, Error, Warning, Selected, Focus, Highlight) and brand colors (Dominant, Texture and Accent).

Starting with Ground of the Base color.

Ground, a white background turns dark gray and not black

It is important to understand that Dark Mode is not the same as 'Inverted Colors'. So white does not become black and black does not become white. If you do so, a number of problems occur:

  1. A black background creates smearing on OLED screens. An OLED screen turns off pixels that are black to save as much energy as possible. Colored, gray or white pixels (partially) turn on the light for that particular pixel again. And because switching on and off takes time, scrolling will cause smearing: white text or other elements will be spread over your screen.
  2. A black background ensures that you can no longer create depth with shadow. Also in Dark Mode shadow has to darken the background. A white shadow creates something completely different.
  3. White text on a black background is difficult to read. This is because less light enters your eyes, which automatically enlarges your pupils and causes more Astigmatism. It seems as if white text on a black background gets a 'glow'. To be readable, text in Dark Mode must have much less contrast than in Light Mode.

The base background, Ground, is not black in Dark Mode, it is dark gray. That could look like this in your CSS:

:root {
    --color-base-ground: hsl(0, 0%, 100%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
    }
}

.page {
    background-color: var(--color-base-ground);
}

Low is for area's to group and clarify the hierarchy

With a surface that is slightly darker than the background, designers often group individual elements in a user interface, so that it is clearer what belongs together. Such an area is called 'Box'. An item in a Box is lower than an item outside it. A Box is therefore instinctively deeper. And what lies deeper should get a darker color. A Box should not receive too much attention. It only helps, is not required to use a user interface and therefore has no contrast requirements.

This requires a color that is not black in Dark Mode, otherwise a shadow will no longer work, yet it should be darker than Ground. I call this color 'Low'. Low is darker than Ground in Dark and in Light Mode.

:root {
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
    }
}

.box {
    background-color: var(--color-base-low);
}

Moderate for non-essential lines

In Dark Mode, you can only choose so many shades between Ground and Black to create distinction even for people with good vision. At some point you have to choose to go lighter than Ground. That is Moderate. Moderate is of course darker than Ground and Low in Light Mode and lighter than Ground and Low in Dark Mode. You can use this for non-essential lines in the user interface.

:root {
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
    --color-base-moderate: hsl(0, 0%, 82%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
        --color-base-moderate: hsl(0, 0%, 31%);
    }
}

.separator {
    border-color: var(--color-base-moderate);
}

High for relevant shapes

High is the first color to have a contrast requirement. Minimum contrast with Low and Ground is 3. This contrast value is good enough (WCAG 2.0 level AA) for graphic elements or user interface elements such as a button or input field.

:root {
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
    --color-base-moderate: hsl(0, 0%, 82%);
    --color-base-high: hsl(0, 0%, 55%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
        --color-base-moderate: hsl(0, 0%, 31%);
        --color-base-high: hsl(0, 0%, 45%);
    }
}

.text-input {
    border-color: var(--color-base-high);
}

Important for necessary shapes

Important is another color with contrast requirements. Minimum contrast with Low and Ground is 4.5. This contrast value is good enough for lines, areas and for larger texts (WCAG 2.0 level AA). Although texts and headings have been given their own set of variables. More on that later.

:root {
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
    --color-base-moderate: hsl(0, 0%, 82%);
    --color-base-high: hsl(0, 0%, 55%);
    --color-base-important: hsl(0, 0%, 44%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
        --color-base-moderate: hsl(0, 0%, 31%);
        --color-base-high: hsl(0, 0%, 45%);
        --color-base-important: hsl(0, 0%, 57%);
    }
}

.icon {
    fill: var(--color-base-important);
}

Critical for elements that must be seen in all situations

This is the shade with the highest contrast requirement. Critical will have a minimum contrast value of 7 with Low and Ground. This ensures that everything with shade Critical complies with WCAG AAA. In practice, you will mainly use this for text with a special purpose, such as 'feedback text' that you want to display in an error color (red).

The steps ends here.

:root {
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
    --color-base-moderate: hsl(0, 0%, 82%);
    --color-base-high: hsl(0, 0%, 55%);
    --color-base-important: hsl(0, 0%, 44%);
    --color-base-critical: hsl(0, 0%, 32%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
        --color-base-moderate: hsl(0, 0%, 31%);
        --color-base-high: hsl(0, 0%, 45%);
        --color-base-important: hsl(0, 0%, 57%);
        --color-base-critical: hsl(0, 0%, 71%);
    }
}

.button {
    background-color: var(--color-base-critical);
}

Root is black or white

You may have noticed I skipped Root. Root is more of a formality. The Root is no different from white in Light Mode and black in Dark Mode. You may encounter unique situations in which you still need it.

:root {
    --color-base-root: hsl(0, 0%, 100%);
    --color-base-ground: hsl(0, 0%, 100%);
    --color-base-low: hsl(0, 0%, 96%);
    --color-base-moderate: hsl(0, 0%, 82%);
    --color-base-high: hsl(0, 0%, 55%);
    --color-base-important: hsl(0, 0%, 44%);
    --color-base-critical: hsl(0, 0%, 32%);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-base-root: hsl(0, 0%, 1%);
        --color-base-ground: hsl(0, 0%, 16%);
        --color-base-low: hsl(0, 0%, 11%);
        --color-base-moderate: hsl(0, 0%, 31%);
        --color-base-high: hsl(0, 0%, 45%);
        --color-base-important: hsl(0, 0%, 57%);
        --color-base-critical: hsl(0, 0%, 71%);
    }
}

Text colors from Primary to Quaternary

Although you can of course use High, Important and Critical for text, text and headings have their own set of variables:

:root {
    --color-text-primary: hsla(0, 0%, 0%, 0.99);
    --color-text-secondary: hsla(0, 0%, 0%, 0.66);
    --color-text-tertiary: hsla(0, 0%, 0%, 0.55);
    --color-text-quaternary: hsla(0, 0%, 0%, 0.42);
}

@media (prefers-color-scheme: dark) {
    :root {
        --color-text-primary: hsla(0, 0%, 100%, 0.65);
        --color-text-secondary: hsla(0, 0%, 100%, 0.55);
        --color-text-tertiary: hsla(0, 0%, 100%, 0.45);
        --color-text-quaternary: hsla(0, 0%, 100%, 0.34);
    }
}

.content {
    color: var(--color-text-primary);
}

You can see that the opacity ('a' stands for alpha channel) has been adjusted instead of the lightness, because it works better on a colored background. In this way it subtly takes over the background color. You could also apply opacity to the other colors, as long as the contrast values ​​are correct.

Text on moderate, high, important and critical is black or white

The default text colors are intended for large areas that have a Ground or Low background color. If you have a control, such as a button, with for example Critical as background color, problems arise with the default text colors. The contrast is too low, resulting in poor readability, and you will see that it does not look pretty. In these cases: text is white by default until the contrast falls below 3, only then the text color should change to black.

If you are familiar with the WCAG 2.0 requirements, you know that you should always aspire to have a contrast value of 4.5, or even 7, and certainly not 3. Yes and no. Ericka Seastrand conducted a mini case study in 2019 with which she found that even people who are colorblind prefer white text over black text in a button, even though the contrast is much lower. The results are not black and white, which is why I definitely recommend reading her study.

Rename your colors before adding Dark Mode

You don't have to use Dark Mode immediately. You can start standardizing your color names and using CSS variables without your client knowing. You do this step by step and when you are done, you can add Dark Mode in no time.

Work in progress

This version of Dark Mode is of course not set in stone. I want to take you through the considerations that have been made and how this version of Dark Mode came about. Copy it 1-on-1 or improve your own version with it. If you play around with it and have improvements, let me know.

Bart van de Biezen

As a cognitive ergonomist at Aan Zee Communications, I do research, design, speak and write about user interfaces and usability. My background: Industrial Design and Psychology at the University of Twente, graduated at Philips in midair pointing for the next generation TV's, Apple Design Award for CSSEdit, usability researcher at MetrixLab and blogger.

You can contact me via email or Twitter, GitHub, or Flickr.