PROGRAMMING
Why Dark Mode Is Hard
And How You Can Implement It On Your React Website

I love me some dark mode.
Hence, when I build websites for my projects these days, I make sure to include it.
It’s generally been quite simple, until I had to do it for my company’s website.
Turns out flipping some colors can be quite a task after all. Now try explaining that to your boss…
Don’t believe me? Here’s YouTube loading dark mode:

See that flickering? Turns out getting rid of that isn’t the easiest thing in the world.
From a blog post I read the other day:
“Maybe the hardest / most complicated part of building this blog was adding Dark Mode.
Not the live-embedded code snippets, not the unified GraphQL layer that manages and aggregates all content and data, not the custom analytics system, not the myriad bits of whimsy. Freaking Dark Mode.”
If you’re confused, that just means you haven’t come across server-side rendering. Or, in simpler terms: you haven’t used Gatsby.
So today I’ll be going through my experience implementing dark mode on a Gatsby website, hoping I can both help people who are looking to do the same, as well as cover some concepts of the “new web” that are worth knowing about.
Thus, even if you don’t use Gatsby (or a similar framework), you should still get quite a bit from this article.
Why Is Dark Mode Hard?
Dark mode is relatively straightforward (notice I didn’t say easy) if you overlook a few minor UX issues.
Primarily, the more nuanced task regarding dark mode is not actually toggling between themes, but rather storing user preferences about which theme users prefer and handling that accordingly on new sessions (while maintaining a good UX).
For example, let’s say you add a dark mode toggle on your website but don’t care about the user’s theme preference when you load the page.
Your logic is then quite simple:
User accesses the page → Website is in light mode → They can toggle on dark mode if they want to
This works fine, but can be a bit annoying to users. After all, we have localStorage
for this very reason — I can store data in the user’s browser and use that to provide better UX, such as remembering what theme they prefer.
So now let’s say you want to improve your website’s UX and decide to store what theme the user prefers.
That’s quite a simple task.
Then, when the page loads, you retrieve the user’s preference and apply it. Or you set it to a default theme if no preference is set.
Simple enough, right? Let’s take a look at what a very simple implementation of this might look like in a React component:

With the snippet above, we get the user’s preference from local storage and use that to set the class of a wrapping div
that can then be referenced in our CSS to style the inner contents according to the theme. In Sass this might look like:

If you haven’t used Sass, the nested structure represented above is syntatical sugar for traditional CSS selectors like
div.light h1 {}
Importantly, this happens before the component renders, so by the time it does render for the first time, it is styled appropriately.
Now, if you try this with Gatsby, your build will fail. As mentioned before, that’s because of Server-Side Rendering (SRR).
What the hell is SSR?
When you use React, you build components using JavaScript and JSX which need to be “evaluated” in order to create and update the DOM (via React’s virtual DOM).
Because of this, your components need to be rendered before they are displayed on the UI. Traditionally, this rendering is done client-side i.e. in your user’s browser.
However, this can also be done on a server. This is becoming increasingly popular, with frameworks like Gatsby implementing it by default and others like Next.js also giving you the option to do so.
The reason for doing this is simple: it makes loading the website faster. Since the server pre-renders the page, it serves the page ready to the client, decreasing the time it takes the page to load.
However, this comes at the expense of web APIs, such as the window
interface, which includes localStorage
. Think about it: if the page is being loaded on the server, how can we know what’s stored in the user’s browser?
A straightforward solution
With the knowledge that localStorage
is inaccessible during the first render with SSR, one might then suggest the use of something like a useEffect
hook or componentDidMount
to apply the changes after the component first rendered.
While this works and can be a valid solution for certain problems, it causes a new issue: it re-renders the component.
Re-rendering can be fine, but it is especially problematic for dark mode. The reason for this is that if your user's preference differs from the default theme, your website will get jumpy like the YouTube example from earlier.
For example, let’s say your default theme is ‘light’, but your user has a stored preference of ‘dark’. When the page is delivered to the client pre-rendered, it will have a light theme, albeit briefly, and then quickly jump to dark once useEffect
triggers.
The “Hacky” Way
Fortunately, not all is lost.
You can still have a website with server-side rendering without a jumpy dark mode. It is a little “hacky” though.
Essentially, you need to insert a script
tag right after your body
tag that runs a script immediately to toggle the class name of your body and determine the logic for switching from light to dark mode.
This is done using the dangerouslySetInnerHTML
attribute for DOM elements, which allows you to insert arbitrary HTML (and scripts) into the page without going through React (hence it’s “dangerous”).

By doing so, we ensure that the body
has loaded so we can modify its attributes, but block the rendering of the rest of the page while the script runs. Then, by setting the appropriate class, our page will render with the designated styling we set for each theme.
Here’s an example snippet of what you might include inside __html
:
Snippet taken from here.
That’s not all folks!
The point of this article is not to be a follow-along tutorial but just a quick overview of the concepts relating to the implementation of dark mode with SSR.
For a great tutorial you should check out Implementing dark mode in React, which goes into more depth on the concepts as well as provides you with the snippets to make this solution work, which I’ve omitted here for the sake of brevity.
However, even after sorting out all this, you’ve still got a lot of work to do, such as writing CSS to change the colors of every element that needs to update with the theme (in my case, I had to tinker with about 40 selectors).
So yeah, even after you have the basic logic in place, you’ll still need to put in more effort to make sure you have a sleek dark mode that works nicely.
Finally, if you’re wondering how our dark mode turned out, here’s a sneak peek of v1:

And, for us, the battle doesn’t end there.
Even with dark mode implemented correctly, our website is still a bit jumpy, also because of an SSR-related issue.
The website contains some hangover boilerplate code that uses a MediaQuery
component from react-responsive
to power most of our responsive layout, and, since you can’t know the window size on the server, that’s also a no-go with SSR.
And so the refactoring continues… (Edit: Now fixed! 🎉)