Dark themes are something I’ve sought since my early days on a computer. I remember finding a way to apply a black background to Microsoft Word 2003, then being horrified when I accidentally printed the document. In uni I scripted my desktop to boot into f.lux’s darkroom mode. One of my only lasting contributions to Signal was loudly advocating for dark mode on iOS and Desktop, not just Android. I even made a browser extension in 2015 to apply a dark theme to the entire Internet.
It’s not the most pressing or exciting work, but I decided my site should have a dark theme of its own. I took a lot of inspiration from my former Khan Academy teammate Josh W. Comeau’s dark mode post, with a few differences.
UI-wise, I decided to go a different route than a binary toggle between dark and light. There were three motivations behind this. From most to least important:
- I wanted to include a “system” option. Theming is still a fresh concept for most web users, so I think it’s worthwhile to expose a way to control theme, motion, brightness etc from within the webpage. But there should always be a simple way to revert back to your system’s display preferences.
- I may add more options later (greyscale? low-res? melty??) and this leaves my options open and unlimited.
- I liked the animated eyeball idea.
Speaking of the animated eyeball… I’m happy with how it turned out! It was quite a deep dive, since I didn’t want to include a heavy animation library like react-spring (>25kb) or Framer Motion (>90kb). Thankfully, the coolest thing I learned while working on this feature is that SVG has a declarative animation specification called SMIL! It didn’t take long to pick up, and it’s very easy to work with. I’m really happy to add it to my toolkit.
Animations were the coolest deep dive, but they weren’t the deepest. Back when I started building this website, I split the header into Cyan, Magenta, and Yellow (CMY) channels. They combined subtractively to make a black-ish color, which I used across the site for text. I removed that feature in late 2019, but recently added it back.
With a single theme, that’s three combinations, for a total of 7 colors. With two themes… the combinations explode a bit.
I use mix-blend-modes to combine colors, of which there are limited options. For my initial color palettes, I wanted to pick three pretty primary colors (light: CMY, dark: RGB) that would blend into three pretty secondary colors (light: RGB, dark: CMY), and all blend together into something with enough contrast from the background to use as body text. Tall order! So I built a tool for that, and spent a few idle hours tweaking settings until I was happy.
And the initial palettes are… pretty good? I wish I was more thrilled with the result, but there are so many conflicting requirements that it’s impossible to get a result as good as if I’d hand-picked the colors independently. I’ve done an initial a11y pass of the colors, and there are some areas of the site where I want to improve the contrast. So I’ll probably end up with a palette split between the “true” colors, and their slightly modified, more attractive cousins.
I also needed to add a global settings provider in this commit. Now that I’ve done the work for that, look forward to more settings (reduce motion, contrast preference, block embeds… cheat codes??) coming soon!
I made a few minor changes while working on this commit:
- Update a bunch of the site doodles to fit either theme.
- Share theme colors between CSS-land and JS-land. No more mismatches!
- Keep the
<Layout>component mounted between pages. Transitions and performance will be just sliiightly smoother.
- Clarify the color variables a bit;
- Add theme-dynamic color variables. For instance,
--color-p1(“primary one”) maps to
--color-redin dark mode and
--color-cyanin light mode.
- Update some non-standard colors (eg. scaling faders demo) to fit the new palettes.
- Super simplify state-handling in the site nav (fixes a persistent console error).
- Standardize the handling of
Hope you enjoy!