Title: Dark or Light: choose one Date: 2020-01-15 Category: Tags: Slug: dark-light-theme Lang: en Summary: How I implemented dark-light theme switcher in this blog. 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][@sheenobu], who took the time to answer to my message and make all this happen[^1]. [@sheenobu]: https://mastodon.social/@sheenobu/103149905102239911 [Here you have some documentation][mdn-prefers-cs] 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](mdn-custom-pro) (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 Firefox[^2], 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 JavaScript[^3]. > **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 implement[^4]. > 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][a-dark-theme-in-a-day] 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 Medium[^5]. 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 post[^6]. 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: ::css 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: ::clike 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: ::clike 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: ::clike 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: ::clike 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 `body`'s `background-color`. This is the updated `getCurrentColor` function: ::clike 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: ::clike 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: ::css 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: ::clike 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. ::clike 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: ::clike 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](https://nomedium.dev/). Also, some interesting free software projects decided to migrate away from it and wrote about it, that's [the case of ElementaryOS][elementary]. [I asked in the fediverse about this][medium-fedi] and many people sent me articles and links. Thanks to all! [^6]: Too bad Marcin, you were unable to see the future. [mdn-prefers-cs]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme [mdn-custom-pro]: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties [a-dark-theme-in-a-day]: https://medium.com/@mwichary/dark-theme-in-a-day-3518dde2955a [elementary]: https://blog.elementary.io/welcome-to-the-new-blog/#the-decline-of-medium [medium-fedi]: https://mastodon.social/@ekaitz_zarraga/103498743276277093