Disclaimer
If you are an experienced developer, you might find this article basic.
My solution to the dark/light theme with CSS, JS, and React. You can read about my thought process and how I implemented it with code examples. There are better ways to achieve the same result, but this is my approach given my current level of programming knowledge.
Thought Process
Since I've built my blog with React and the Next.js framework, there wasn't a specific reason for me to implement the theme switcher using plain CSS without any JavaScript. I utilise Server-Side Rendering and Server-Side Components, where the HTML & CSS are rendered on the server and sent to the client. Certain parts of the navigation and a few other elements rely on React hydration, but users can view the full UI with HTML and CSS, including the theme, even with JavaScript disabled.
In a progressive enhancement sense, this is the flow:
- On request, HTML & CSS are sent to the user by the server.
- If the browser is an old fella, the light theme custom properties values will be applied as default.
- If the browser is a modern buddy and it gets the
prefers-color-scheme
CSS query, we detect what the user's operating system preferences are and then with CSS apply the corresponding variation of custom properties values, either light or dark theme. Up until here, we do not need JS at all. - After the initial load, the header component will be hydrated. Once done, we use React's useEffect() hook to ask the same question: What theme version is your OS default? This time, however, we utilise JavaScript instead of CSS. Once we know the answer, we update the React state and with that, we synchronise the user's OS preferences with the app state, ensuring that they are the same.
- The final piece is a button that offers the user option to toggle the theme from light to dark and vice versa if he wants and at any moment. This action triggers another useEffect() and a re-render, flipping values in our colours custom properties with JS...
Code
Let's see all in more detail... and with code examples.
CSS
If a user has JS disabled, and is using an old browser it will read only :root
. If the browser is modern, using @media (prefers-color-scheme: dark)
we apply correct theme based on his OS preferences:
I use custom properties for colours, and since the light theme is chosen by the majority of users as default, that’s what’s in the :root. Older browsers that do not understand @media (prefers-color-scheme: dark)
can load variables fine, and the default light theme will be applied.
Inside @media (prefers-color-scheme: dark)
, we have the same custom properties with different values for the dark theme, and any user with modern browsers can have the dark theme applied.
With this code, we have respected user preferences in their operating system settings. No JS is necessary. That’s fine. Old browsers get the light theme, modern ones as per OS defaults chosen by the user.
React and JS
If the user has JS enabled, the header component will hydrate, and we can use the useState() and useEffect() hooks to implement the theme toggle.
Using the React useEffect() hook, we check via JS the default user OS theme. We do that to synchronise our app state with the user's OS preference and with what is applied on the page by CSS.
Once we are all good and synchronised, and if the user wants to endlessly click the button that switches the theme, let’s give him that pleasure.
Inside the second useEffect()
hook, and using the JS .setProperty()
method, we flip the theme."
That’s about it...
Final words
I know that this can be done much better, but let’s reserve that for future adventures once I progress more with my knowledge.
Doubts
First, I use two useEffect() hooks. (Edit: Changed to one...) That could be squeezed into one with an if statement inside and another useState() named isFirstRender(true). In that case, the code from the first hook where we sync the state will be placed inside the second hook. I’ll try that soon to see what happens afterwards.
A week later...
I've done it; placed all in one useEffect() and it looks to me that it works better, here is the final code:
The second doubt is about setting a cookie in the browser with user theme preferences and loading that cookie on subsequent visits. However, this complicates things.
I wonder how many users prefer a different theme to their OS default. In my opinion, not many. If this blog were a large app used daily, then setting a cookie would be fine. But for my needs, it is overkill.
Additionally, I do not want to set any cookies because I’m located in the EU, and I do not want that cookie banner thing to pop up on my personal site. No tracking, no cookies, no legal need for a cookie banner.
Lastly, I have not noticed any flash of unstyled HTML or the wrong theme. That doesn’t mean I’m 100% sure there is no flash at all.
Thanks.