summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content/dark_light_theme.md425
1 files changed, 425 insertions, 0 deletions
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