Responsive Color Schemes with an Override Switch

One of the features on my new website is it now honors your browser’s color scheme preferences using the new prefers-color-scheme media query. With more operating systems adopting dark modes, this is a nice new feature that I hope more website will implement.

I recognize that a responsive color scheme might not be for everyone at this point and not all browsers currently support this media query, so an override switch is also a good idea. Read on for how I implemented this feature.

Solution

The best way to implement a user preference would be in localStorage which we can simply read in a small script tag in the head that simply sets a data attribute we can use as a CSS selector.

1
document.documentElement.setAttribute('data-theme',(window.localStorage||{}).theme||'')}

Now we can setup our SCSS mixins to take advantage of our style selector if present, or using the default.

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
$default-theme: dark;

// For things the apply to any theme, same specificity as dark and light.
@mixin theme-any() {
html &,
html[data-theme] & {
@content;
}
}

// Dark theme only.
@mixin theme-dark() {
@if $default-theme == dark {
html & {
@content;
}
}
@else {
@media (prefers-color-scheme: dark) {
html & {
@content;
}
}
}
html[data-theme='dark'] & {
@content;
}
}

// Light theme only.
@mixin theme-light() {
@if $default-theme == light {
html & {
@content;
}
}
@else {
@media (prefers-color-scheme: light) {
html & {
@content;
}
}
}
html[data-theme='light'] & {
@content;
}
}

Note the default theme variable, which I have set to dark, controls what will be used if the browser does not specific a preferred scheme.

Now when you want to apply CSS values to a specific theme only, you can use those mixins in your selectors.

1
2
3
4
5
6
7
8
9
10
body {
@include theme-dark() {
color: white;
background: black;
}
@include theme-light() {
color: black;
background: white;
}
}

Now in JavaScript all you need to do to change the theme is set the data attribute and update the localStorage.

1
2
3
4
5
6
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
if (window.localStorage) {
window.localStorage.theme = theme;
}
}

Notes

There’s a small amount of duplication of CSS rules as a result of this, but Gzip will compress those down to practically nothing.

If you are using postcss to process your CSS, you’ll probably want to disable the prefers-color-scheme-query feature unless you plan to also load the associated JavaScript polyfill (useless in this setup).

Comments