I thought adding dark mode would take 30 minutes. It broke my website.
Problem 1: Hardcoded colours
I didn’t even use that many colours. How hard could it be? Turns out, very.
My colours were hardcoded directly into Tailwind utility classes. A heading had a specific hex value, and a paragraph had another. To change the theme, I would have had to update each of these individually. No, thank you.
The fix
I introduced CSS variables and defined colours by role, not value.
My first attempt had variables like hover-dark and hover-darker. That worked until I tried to invert the theme for dark mode. What would hover-darker even mean in dark mode?
Instead of asking "What color should this element be?", I had to ask "What role does this color play?"
So I switched to variable names that were semantic, rather than literal:
--text-primary--text-secondary--background--border
With this, a heading wasn’t “black” anymore. It was text-primary. A background wasn’t “white”. It was background-primary.
At this point, I thought I was mostly done. I wasn’t even close.
Problem 2: Dark mode is not white on black
I thought dark mode meant white text on a black background. It looked terrible. Who would've thought that having white text on black would feel so... bright? It was harsh, hard to read, and everything started blending together - almost like I had suddenly developed astigmatism.
The fix
It turns out dark mode isn't really black and white. It's shades of grey. Instead of white, I used a light grey and text was miraculously legible again. For secondary text, even lighter grey worked perfectly.
Pushing contrast to the extreme with white text on black backgrounds was a disaster. Tuning that contrast and making it proportionate and layered made things way more comfortable.


Subtle change to reduce contrast, making it easier to read over a longer period of time
And that's all my problems solved, said no one ever.
Problem 3: Everything breaks differently
Even after fixing colours, things still looked off. Different parts of the UI broke in different ways:
Problem 3.1: Typography (Tailwind)
Tailwind’s typography plugin (prose) worked great in light mode. But once I introduced my own variables, the defaults started conflicting with my system. Some styles updated, others didn’t. Fixing one thing broke another. The abstraction broke down, and the complexity I’d tried to hide came rushing back.
The fix
I explicitly mapped Tailwind’s typography variables to my own. Instead of relying on defaults, I treated typography as part of my system.
Once everything pointed back to the same set of variables, things became predictable again.
Problem 3.2: Code Syntax Highlighting
I use a lot of code snippets, especially in my JavaScript event loop article series. For syntax highlighting, I had used Github's light theme for my light mode. Too bad it was impossible to read in dark mode. So I switched to Github Dark CSS - only for it to look off in light mode.
The fix
For a while, I assumed I had to pick one. Eventually, I realised the obvious solution: use both Github and Github Dark CSS and switch dynamically based on the mode. It sounds pretty obvious now, but at the time, I genuinely thought I had to choose.

Github's light theme in dark mode was impossible to read

Github's dark theme in light mode looked washed out
Problem 3.3: Images
Images introduced a different kind of problem. Some worked fine. Others didn’t translate at all.
My hero image is of a sunrise. From the start, I imagined using a sunset version for dark mode. Did I create dark mode just so that I can use this image? Maybe.
Thankfully, this was easily implemented by including both images and switching between them based on the mode.
But my SVG diagrams were harder. I tried making their colors dynamic using CSS variables but it didn’t work reliably.
The fix
Instead of forcing everything to be dynamic, I created two versions of each diagram, one for each mode. It felt less elegant at first, but it worked better. Not everything should be dynamically styled.

Diagrams designed for light mode don’t translate automatically.
Problem 4: The flash
After fixing everything, I refreshed the page. A flash of light mode appeared before it switched to dark. It was subtle, but definitely there. And yes, the temptation to pretend that didn't happen was definitely there too.
The browser was rendering before the correct theme was applied. By the time JavaScript the correct theme was set, the browser had already painted the wrong one.

The flash: light mode renders before dark mode is applied
The fix
The theme needed to be determined before rendering. Moving the theme logic earlier removed the flash entirely. It was a small change technically, but it had a big impact on how the site felt.
The real lesson
I thought I was adding a feature: a toggle button and a visual enhancement that sits on top of everything else.
But dark mode didn’t sit on top of my UI. It ran through it and every part of the system had to agree. None of the above was individually difficult. But together, they revealed that dark mode was a system and one that needs to be designed intentionally.
If you’re implementing dark mode
A few things I wish I knew earlier:
- Define colors by role, not value
- Avoid extreme contrast
- Treat typography as part of your system
- Don’t force everything to be dynamic
- Handle theme selection before render
If you’re curious, the full implementation and visuals are on my site. This article was also originally published there.
Top comments (0)