1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
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
|