So, I started fooling around with a django app since the last time, and noticed the debug toolbar1 had added a theme switcher (this may be old news. It's been a while since I started a django project).
Since I had just fooled around with it myself only a couple days before, I took a look at their implementation.
It's very similar to what I had done (which I scraped together from various examples), but slightly shorter and a bit more elegant.2
So obviously I started fooling around with it and began adapting it to what I wanted. Along the way I fell into the overengineering trap and turned it into some kind of library.
Here's what I ended up with:
// Lifted and adapted from django-debug-toolbar:
// https://github.com/django-commons/django-debug-toolbar/blob/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/static/debug_toolbar/js/toolbar.js#L215
var themeToggler = (function(w, d, undefined) {
const defaultOpts = {
switcherElementSelector : "a#theme-switcher",
rootElementSelector : "body",
localStorageVarName : "user-theme",
dataThemeAttrName : "data-theme",
lightThemeName : "light",
darkThemeName : "dark",
autoThemeName : "auto",
callback : undefined,
};
const prefersDark = w.matchMedia("(prefers-color-scheme: dark)").matches;
// Those will be set from opts in the init function and therefore can't be
// const.
var themeList = undefined;
var rootElement = undefined;
var userTheme = undefined;
const setTheme = (theme, opts) => {
rootElement.setAttribute(
opts.dataThemeAttrName,
theme === opts.autoThemeName
? (prefersDark ? opts.darkThemeName : opts.lightThemeName)
: theme
);
if (opts.callback) {
opts.callback(theme, opts);
}
};
return {
init: (_opts) => {
const opts = {...defaultOpts, ..._opts};
themeList = prefersDark
? [opts.lightThemeName, opts.darkThemeName]
: [opts.darkThemeName, opts.lightThemeName];
rootElement = d.querySelector(opts.rootElementSelector)
userTheme = localStorage.getItem(opts.localStorageVarName) || opts.autoThemeName;
d.querySelectorAll(opts.switcherElementSelector).forEach((el) => {
el.addEventListener("click", (ev) => {
const index = themeList.indexOf(userTheme);
userTheme = themeList[(index + 1) % themeList.length];
localStorage.setItem(opts.localStorageVarName, userTheme);
setTheme(userTheme, opts);
ev.preventDefault();
});
});
setTheme(userTheme, opts);
}
};
}(window, document));
I followed what I'm half remembering from the best practices of the 2010's3, and it looks like an old jQuery plugin as a result.
I completely stopped following whatever was going on in the Javascript world about ten years ago. Hell, even the arrow function syntax feels new to me. So I have no idea how actually usable this is in the current ecosystem.
I don't care, though. For what I do, copying and pasting scripts is more than enough, so this will do for now. Maybe I'll have a look at web components and see if it makes sense to turn this into one, though.
I don't feel too bad about going overboard with the parameterization. The actual logic is still pretty simple, and dark mode is a nice usability feature, so having some drop in snippet to use in a future project could come in handy4.
For comparison, here's what I'm currently using on this site:
function getTheme() {
var theme = localStorage.getItem('theme');
if (!theme) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
localStorage.setItem("theme", theme);
}
return theme;
}
var stylesheetLight = document.getElementById('syntax-style-light');
var stylesheetDark = document.getElementById('syntax-style-dark');
function switchTheme(theme) {
var body = document.querySelector('body');
if (theme == "dark") {
body.classList.add('theme-dark');
stylesheetLight.media = "not all";
stylesheetDark.media = "screen, projection";
} else {
body.classList.remove('theme-dark');
stylesheetLight.media = "screen, projection";
stylesheetDark.media = "not all";
}
localStorage.setItem("theme", theme);
return theme;
}
var theme = getTheme();
switchTheme(theme);
var themeSelector = document.querySelector("button#theme-switcher");
themeSelector.addEventListener('click', function(ev) {
theme = switchTheme(theme === 'dark' ? 'light' : 'dark');
ev.preventDefault();
});
It's shorter and more to the point, but also coupled to this particular site's markup, which means it'll need some editing to be useful elsewhere.
It's also taking care of some specialized logic (switching the styles for syntax highlighting) which many sites probably won't need, which is what prompted me to add a callback option to the reusable version.
Adding an option to switch between setting a class or an attribute could be nice, but hey. Maybe I'll do that at some point, but that's good enough for now.
The bug that wasn't a bug
As I dug into the debug toolbar code, I spotted what I took to be a minor bug, and dilligently opened an issue about it. Turns out it's not a bug, and I learned something in the process. Sweet!
In short, they're offering three options: light, dark, and auto, the last one being mapped to the user's system's preferences, which can change between two visits.
The issue is that toggle buttons with more than two states feel weird to use, and in this case you end up with one out of three clicks effectively doing nothing.
Having the third option is the right thing to do from an usability standpoint, but the UI for it should probably be a dropdown list (or a simple bunch of dumb links I guess). This can be harder to integrate within a broader design, though.
If you read my code, you'll have noticed that I kept only two states. This barely changes the logic (adding a third one would only involve modifying a couple of lines), but I don't want to bother implementing a nice dropdown. I think ignoring the user's potential changes in preferences isn't too bad for a simple website, so I'll keep things nice and simple.
Still, I'll have to at least think about this on future projects, and my snippet might turn out not so great for the ones that should include the "System Settings" option. If I ever write a doc for it, it will need to start with this disclaimer.
My first real article was an attempt at demonstrating how designing a good interface requires a lot more thought than simply going for maximum bling. This whole thing is a nice reminder of this.
Javascript Apocalypse
For a self proclaimed back-end dev, I seem to be posting a lot of javascript on here. Part of the reason why is that this site being static, I don't have a backend when I'm fooling around with it.
I have this weird love / hate relationship with javascript. The language's quirks often drives me crazy and I'm appalled at how it seems to have conquered the whole world5, but somehow working with it tends to feels nice. I don't know why, but I just enjoy it. I'm strangely fascinated by prototypal inheritance, even though I've barely used it, and I love the language's history, too. Even its name doesn't make sense.6
Also, you can do shit like this:
const switcher = document.querySelector('button#theme-switcher');
// I tried a few different values and chose that one for maximum annoyance.
// Don't ever tell me I don't suffer for my art.
const delay = 150;
function epilectic_jamboree() {
switcher.click();
setTimeout(epilectic_jamboree, delay);
}
setTimeout(epilectic_jamboree, delay);
I almost put a prank link to activate this on this page. Oh, you know what would be even worse ? Choosing a random link on each page load and pointing it to this abomination. Now that would be pure evil. Pray that I never get bored or pissed enough to do it.
Allright, so between getting sidetracked with this whole js thing, reading up on UX and writing this article, all I have to show for yesterday's work is a mostly blank html page. Which can switch colors, I guess.
I hope my boss won't mind.
-
One of the first things I install whenever I start anything with django. I barely ever look at it but it's invaluable to have it set up when something inevitably goes wrong. ↩
-
Using modulo to cycle through list indices (ie:
themeList[(index + 1) % themeList.length]
) is one of those simple tricks I wish I thought about more often. ↩ -
Self-calling anonymous functions, yay \o/ I remember how weird those felt when I first saw them. ↩
-
I guess adding some docs would still be a good idea, tho. ↩
-
If some general AI ever takes over the world, chances are it will run on javascript. This fact is both reassuring and terrifying. ↩
-
As usual, marketting ruins everything. ↩