From 4143a4f70fe8820c898a027f24ca43dee7b289f1 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Sat, 18 Jan 2020 22:44:14 +0100 Subject: dark-light theme post --- content/dark_light_theme.md | 425 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 content/dark_light_theme.md diff --git a/content/dark_light_theme.md b/content/dark_light_theme.md new file mode 100644 index 0000000..7a744a9 --- /dev/null +++ b/content/dark_light_theme.md @@ -0,0 +1,425 @@ +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 -- cgit v1.2.3