Ekaitz's tech blog:
I make stuff at ElenQ Technology and I talk about it

Dark or Light: choose one

Since this afternoon this blog has a way to change between dark and light themes.

I made this because my eyes hurt when I visit really light websites from a window with a dark background. My desktop environment is configured to show everything with a dark background and I spend most of my time on the terminal so my eyes get used to the dark background and the light ones hurt, specially at night.

I realized one of the sites that made my eyes hurt was my own website and I can’t fix the whole web, but at least I can fix my site and write down what I did to encourage you to fix yours.

User preference

First things first, since 2019 CSS has a new mediaquery that lets you know if the visitor has a dark or a light background configured in their system. I was introduced to this thanks to @sheenobu, who took the time to answer to my message and make all this happen1.

Here you have some documentation about that mediaquery called prefers-color-scheme, but in summary it can take three values —light, dark and no-preference— that are quite self-explanatory in my opinion.

So if you mix that with a little bit of CSS custom properties magic (AKA variables) you can just parametrize the whole color scheme and then use the mediaquery to choose the variables you want to use. That’s fine.

Locked in your preference

The problem comes when you want to be able to let the user change from one color scheme to other.

The prefers-color-scheme mediaquery gets user’s preference but, at least in Firefox2, it’s not easy for the user to change to a light theme if they want. They are locked in what they chose for their OS.

Sometimes it’s interesting to let the readers change the theme by themselves for multiple reasons. As each developer or designer chooses the colors they want, that may led to color scheme inconsistencies between the system colors and the sites or have readability issues. Also, readability is subject to personal preference: I like to use dark backgrounds, but sometimes I prefer to read from a lighter background if the ambient light is stronger.

Letting visitors contradict themselves

In order to let the visitors go against that blood pact they signed with their OS, we need some JavaScript3.

Note about my personal taste: I avoid the use of JavaScript in places that is not needed. I consider it unnecessary for blogs or websites that show you information in a format supported by the web (text, audio, video…). Also, I consider really important to think about the users who don’t want to or can’t run JavaScript.

Most of my sites don’t use JavaScript at all, modern CSS and HTML are more than enough for most of the applications. Webpages with a heavy use of JavaScript are a threat to accessibility and make bots, spiders and non-canonical browsers hard to implement4.
This blog makes use of JavaScript for two different things:

  • The theme change I’m talking about in this post
  • Source code highlighting

In both cases the blog is prepared to work perfectly for users with JavaScript disabled. When the JavaScript is disabled, code blocks respect the HTML tags for code declaration but they have no any extra tags or style. In the case of the theme control, when JavaScript is disabled, the website makes use of the user’s default preference leaving the option to change the theme in hands of the browser or the operating system. Most of the time, these design decisions work in favour of users that access the web from browsers that don’t need any kind of styling (terminal browsers, screen readers…) helping the browser find the content more easily.

When I was going to start implementing it I remembered a Medium post by Marcin Wichary that explains the process very well. I used as a reference but I added a couple of points I want to share with you. I’ll also try to cover everything the author talks about with my own words, just in case someone doesn’t want to access Medium5.

First difference from the reference post is what I told you about in the previous section. The post is from 2018, and the prefers-color-scheme mediaquery is from 2019, so it’s not mentioned in the post6.

The mentioned post has also an introduction to CSS Custom Properties and their use. I already gave you a link to the MDN Web documentation and I don’t feel myself informed enough to try to explain you anything about CSS, so better go there and read.

That said, the first point we have to solve is to have some property that makes CSS know which theme is in use. That can be implemented like the article does, adding a data-something attribute to the html that then can be captured in CSS like this:

html[data-theme='dark'] {
    /*Your dark theme style here*/
}
html[data-theme='light'] {
    /*Your light theme style here*/
}

WARNING: Be careful with the priority of this change, you have to put it after the prefers-color-scheme mediaquery to make the cascade work as it should. If you put it before, the mediaquery is going to override this configuration and will make it pass unnoticed.

But now you have to deal with the attribute and make it change whenever the visitor selects one or other configuration. As I said, you need JavaScript for that. Setting the attribute is as simple as this vanilla JavaScript line:

document.documentElement.setAttribute('data-theme', color);

Good. Now it’s quite easy to start, right? Add a button, put an event listener on it and whenever it’s pressed change the theme by setting the attribute with the line I just show you. For instance:

var theme_switch = document.getElementById('dark-light-switch');

function change(color){
    document.documentElement.setAttribute('data-theme', color);
    theme_switch.setAttribute('color', color);
}
function theme_change_requested(){
    color = theme_switch.getAttribute('color');
    if(color=='light')
        change('dark');
    else
        change('light');
}
theme_switch.addEventListener('click', theme_change_requested);

We selected an element that will act as a theme switcher and added an event listener to it. Whenever it’s clicked it will run the theme_change_requested function that will change the color from the current to the other. Easy.

Problems come now.

Get the initial color

In order to start that process, you have to be able to know the current theme in use, that way you’d be able to activate the necessary attribute for the html tag or the current look of the theme switcher (in this blog a sun or a moon).

This current theme inspection results to be difficult because JavaScript doesn’t have access to the prefers-color-scheme mediaquery. You can bypass that by getting something you know is going to be present in your CSS and reading it. In my case I used the background-color of the body because I set the background to white in the light color scheme as you can see in the getCurrentColor function:

var theme_switch = document.getElementById('dark-light-switch');

function change(color){
    document.documentElement.setAttribute('data-theme', color);
    theme_switch.setAttribute('color', color);
}
function theme_change_requested(){
    color = theme_switch.getAttribute('color');
    if(color=='light')
        change('dark');
    else
        change('light');
}
function getCurrentColor(){
    // This is dependant of the CSS, be careful
    var body = document.getElementsByTagName('BODY')[0];
    var background = getComputedStyle(body).getPropertyValue('background-color');
    if(background == 'rgb(255, 255, 255)') {
        return 'light';
    } else {
        return 'dark';
    }
}
function init( color ){
  change(color);
  theme_switch.setAttribute('color', color);
}
init( getCurrentColor() )
theme_switch.addEventListener('click', theme_change_requested);

Now, with this new code you are able to get the current theme when the page loads and prepare your button and your html tag to start with the color scheme the visitor has configured by default.

Page-change amnesia

Once you have all we explained working you’ll realize the website forgets visitor’s decision when they navigate form one page to another. It makes perfect sense, because there’s no way to keep the selection set.

We can make use of localStorage for this. With the following line we can set the 'color' item in the localStorage to the color visitor chose:

localStorage.setItem('color', color);

Updating the getCurrentColor function, we can get the color from the localStorage first, and, if it’s not set, we can use the strategy we used before with bodys background-color. This is the updated getCurrentColor function:

function getCurrentColor(){
    // Color was set before in localStorage
    var storage_color = localStorage.getItem('color');
    if(storage_color !== null){
        return storage_color;
    }

    // If local storage is not set check the background of the page
    // This is dependant of the CSS, be careful
    var background = getComputedStyle(body).getPropertyValue('background-color');
    if(background == 'rgb(255, 255, 255)') {
        return 'light';
    } else {
        return 'dark';
    }
}

With this function we can know what color has the user configured or the color they chose in our color selector button, but still have to activate the theme if the user has chosen one that is not the one on their preferences. Updating the init and change functions this way is more than enough for that:

function init( color ){
    change(color, true);
    localStorage.setItem('color', color); // CHANGED!
    theme_switch.setAttribute('color', color);
}
function change(color, nowait){
    document.documentElement.setAttribute('data-theme', color);
    theme_switch.setAttribute('color', color);
    localStorage.setItem('color', color); // CHANGED!
}

Smooth transitions

In the article I had as a reference the author made a simple but very effective approach for theme transitions. The article uses the following CSS for smooth transitions:

html.color-theme-in-transition,
html.color-theme-in-transition *,
html.color-theme-in-transition *:before,
html.color-theme-in-transition *:after {
  transition: all 750ms !important;
  transition-delay: 0 !important;
}

The article also explains how to activate the transition, the following piece of JavaScript code activates the transition and deactivates it one second later:

window.setTimeout(function() {
  document.documentElement.classList.remove('color-theme-in-transition')
}, 1000)
document.documentElement.classList.add('color-theme-in-transition');

We have to be careful with where do we add this because we may be forcing transitions in the navigation and that’s really annoying. Updating the change function with the transition is not enough, we need a way to discard the transition for the changes produced by the init function. We can exploit the fact that JS arguments are optional for that. Of course, the transition must be added in the change function.

function init( color ){
    change(color, true); // Add true for nowait
    localStorage.setItem('color', color);
    theme_switch.setAttribute('color', color);
}

function change(color, nowait){ // Add the nowait argument
    // Discard transition is nowait is set
    if(nowait !== true){
        window.setTimeout(function() {
            document.documentElement.classList.remove('color-theme-in-transition')
        }, 1000)
        document.documentElement.classList.add('color-theme-in-transition');
    }

    document.documentElement.setAttribute('data-theme', color);
    theme_switch.setAttribute('color', color);
    localStorage.setItem('color', color);
}

Now with all this we are able to make the website the user configuration from one page to another.

Wrapping up

With this configuration we are able to:

  • Get visitor’s configuration based on the OS color theme: dark or light.
  • Make the visitor able to change their mind by choosing a different color scheme.
  • Get the initial color of the page to be able to initialize the buttons.
  • Make the web remember the color scheme selection from one page to another using localStorage.
  • Add smooth transitions but don’t activate them in page changes to avoid weird flashings.

And that’s all.

No! It isn’t! We also had some fun talking about philosophy, accessibility and sites you shouldn’t visit. In fact, all the color theme stuff was an excuse to talk about it, but sssssssh don’t tell anyone.

If after knowing that you are still interested on the excuse itself, all the code together it looks like this:

var theme_switch = document.getElementById('dark-light-switch');
var body = document.getElementsByTagName('BODY')[0];

function init( color ){
  change(color, true);
  localStorage.setItem('color', color);
  theme_switch.setAttribute('color', color);
}
function change(color, nowait){
  // Discard transition is nowait is set
  if(nowait !== true){
    window.setTimeout(function() {
      document.documentElement.classList.remove('color-theme-in-transition')
    }, 1000)
    document.documentElement.classList.add('color-theme-in-transition');
  }

  document.documentElement.setAttribute('data-theme', color);
  theme_switch.setAttribute('color', color);
  localStorage.setItem('color', color);
}
function theme_change_requested(){
  color = theme_switch.getAttribute('color');
  if(color=='light')
    change('dark');
  else
    change('light');
}
function getCurrentColor(){
  // Color was set before in localStorage
  var storage_color = localStorage.getItem('color');
  if(storage_color !== null){
    return storage_color;
  }

  // If local storage is not set check the background of the page
  // This is dependant of the CSS, be careful
  var background = getComputedStyle(body).getPropertyValue('background-color');
  if(background == 'rgb(255, 255, 255)') {
    return 'light';
  } else {
    return 'dark';
  }
}
init( getCurrentColor() )
theme_switch.addEventListener('click', theme_change_requested);

  1. Thanks for being there! 

  2. The way to change that is to access about:config and update the ui.systemUsesDarkTheme field: 1 means true and 0 means false. Be careful because it’s not a boolean field, it’s an integer field (I don’t know why, don’t ask me). This change affects to all the tabs. 

  3. This isn’t true in every context. We need it here because this is a Static Website. This means the content you read is already created at server side before you ask for it. If it wasn’t, all this could be simpler: just load a different CSS depending on the user’s choice. The static counterpart of this approach would be to create the whole website once per color scheme and leave them in different folders like domain/dark/whatever.html and domain/light/whatever.html this is not practical at all and carries tons of extra problems. 

  4. As everyone want to have a good rank in the search engines more than anything else, Google made a lot of decisions about how websites should be in order to be able to be indexed properly. With the market quota they had (almost 100%) they had the power to force developers and designers make websites the way Google liked. That was obviously bad but it had some good consequences: websites were easy to scrape or read by a robot with low resources (that was what Google wanted). But since some years ago Google announced their spider is able to run JavaScript, that made all those developers and designers who wanted to make their websites have a good ranking free: they don’t have any other limit to the use of JavaScript right now (because they don’t really care about anything else). That made many pages impossible to read by clients that don’t use JavaScript and made the process of accessing websites automatically or with non-canonical browsers impossible in many cases. Thank you developers and designers for breaking the Web

  5. There are so many reasons to avoid Medium that someone made a specific website for them. Also, some interesting free software projects decided to migrate away from it and wrote about it, that’s the case of ElementaryOS. I asked in the fediverse about this and many people sent me articles and links. Thanks to all! 

  6. Too bad Marcin, you were unable to see the future.