Dreaming of UI Theming
Published March 30, 2021
Another announcement-style post: the theming on this site has been improved. There are now 4 discrete themes to choose from along with just letting the prefers-color-scheme
setting cascade.
The four themes:
- Light. The initial theme—mimics the “dark on light” contrast of printed media
- Dark. Improved the previous dark theme to be, well, darker—good for burning midnight oil
- AMOLED. High-contrast—#fff on #000—efficient on AMOLED displays
- DELOMA. The opposite1 of AMOLED—#000 on #fff
Technically, there’s a fifth theme—System—which uses your preference set in prefers-color-scheme
to toggle the dark
theme if appropriate. If you were not previously specifying the dark
theme then you will be using prefers-color-scheme
implicitly with this new change.
Curious how I did it? Read on.
Implementation
High-level overview: using the browser localStorage
to persist the theme
in a key of the same name. On page load, localStorage
is queried for the theme
key and if it exists, it’s added to the html
element with a data
attribute e.g., <html data-theme="dark">
. This JavaScript is executed in the <head>
right before the stylesheet is <link>
ed which prevents the dreaded “flash of incorrect theme” (FOIT). The themes themselves are implemented with CSS custom properties AKA variables scoped to the html
element.
Now, some code snippets.
localStorage
query and set html
attribute in <head>
:
(function (html) {
if (localStorage.getItem('theme')) {
html.setAttribute('data-theme', localStorage.getItem('theme'))
}
})(document.documentElement)
Theme (light
& dark
) implementations in CSS:
html,
html[data-theme="light"] {
--accent: #6d14e9;
--bg: rgb(252, 250, 253);
--fg: rgb(16, 18, 22);
--hover-bg: rgba(0,0,0,0.02);
}
html[data-theme="dark"] {
--accent: #fff91a;
--bg: rgb(16, 18, 22);
--fg: rgb(252, 250, 253);
--hover-bg: rgba(255, 255, 255, .02);
}
I also implemented dark mode images. I opted to apply the dark mode styling when the image is :not()
:hover
ed or zoomed:
@media (prefers-color-scheme: dark) {
/* ... */
html:not([data-theme="light"]):not([data-theme="deloma"]) img:not(.zoom-img):not(:hover) { filter: brightness(.8) contrast(1.2); }
/* ... */
}
html[data-theme="dark"] img:not(.zoom-img):not(:hover),
html[data-theme="amoled"] img:not(.zoom-img):not(:hover) { filter: brightness(.8) contrast(1.2); }
Pain Points
The two primary papercuts I faced were CSS specificity and trying to keep the stylesheet as DRY as possible.
CSS specificity isn’t a new papercut. It’s a common side effect encountered: entire naming conventions like BEM exist to proactively mitigate it. Annoyingly, this presented in the edge case of using a non-dark theme e.g., light
or deloma
while the prefers-color-scheme
queried to dark
. My solution of explicitly negating non-dark data-theme
attributes is ultimately technical debt cruft.
If I used a CSS preprocessor like SCSS I would have been able to use some of the language features like variables, mixins, and functions to help skirt around the specificity and DRYness. At the very least, it’s another layer of abstraction. And, it would have added complexity in the form of a build step since I would need to compile the style.scss
into style.css
. If I were to go down the path of entirely rewriting my blog theme I could see the value in using a preprocessor from the beginning. However, for this “bolt-on” theming implementation I’ll count the vanilla CSS as a win.
Finally, a saving grace would have been Firefox 87 which lets you toggle prefers-color-scheme
in the devtools. Absolutely beats digging through the OS settings to toggle it. FF 87 was released while I began writing this post—better late than never.
As linked in previous posts, the blog theme is open-source. The pull request specifically may be of interest.
Techincally, it’s just the reversed string. I did consider inverting the characters e.g., ROT-13 but it was too gibberish-y↩︎
Last modified March 29, 2021 #announcement #frontend #ui #css #js