React | Open Source | Mobile | Tailwind CSS | NextJS | AccessibilityDark Mode in Next.js using Tailwind CSS and React HooksAugust 3, 2021
Neil Chaudhuri (He/Him)
Neil Chaudhuri (He/Him)
It's quite possible that while waiting for the ads on Hulu to end you stumbled upon the option to set your phone's theme to Dark Mode. Dark Mode is becoming a staple of user interfaces on the web and mobile devices for several reasons-- primarily to ease the strain on your eyes and to reduce battery consumption.
At Vidya we pride ourselves on embracing emerging technologies and helping our clients leverage them to realize their potential. When it came time to give our website a fresh new look, we figured adding a toggle-able Dark Mode option would be consistent with that mission. This website you're reading right now supports Dark Mode. Just look at the top of the page.
If you would like to implement Dark Mode on a Next.js site using TailwindCSS, let me show you how. It involves three key pieces:
Scripttag that we got in Next.js 11
- Understanding, like really understanding, React's
Activating Tailwind's Dark Mode Support
Tailwind CSS offers two ways to set Dark Mode. If you are content to default to system settings, then all
you need to do is confirm your
tailwind.config.js file has the
media setting, which uses the
prefers-color-scheme CSS media feature:
But since we want more control to let Vidya users decide which look they prefer, we need the
class setting instead:
Now you need to handle variants like the TVA in Loki.
Variants in Tailwind define the ways in which you want to apply different styles.
For example, if we want to set a red background on a link hover, we apply the
hover variant on the
As an aside, the CSS equivalent would be this for our shade of red:
We will do similar to apply
dark variants of our branding scheme throughout our interface. For example, here is a simplified
version of our
contact-us class composing numerous Tailwind utilities in Next.js's
Note that you always put
dark first when you have multiple variants like
This is where you will spend most of your time. Mostly because you want to put together a Dark Mode color palette that is usable and accessible and consistent with your branding and because you want to be thorough in applying it throughout the site.
Just remember to extract components as we did above to keep things maintainable, consistent, and organized.
Because we are relying on the Tailwind
class setting for Dark Mode, we need to figure out a way to hook the
dark class onto the
root element of each page like this:
And we need to be able to do it on demand. This is where our code comes into play.
The Script Tag
execution has always been awkward. Where do you put this script on the page relative to that one? Do you put this script at the top of the
or at the bottom of the
body element? It's actually easier figuring out where to seat everyone at your wedding.
In v11.0.0, Next.js introduced the
Script tag, and it makes all this
a lot better. You can put the
Script tag anywhere, and you apply one of three strategies to let Next.js know when it
Before we specify which strategy should apply here, keep in mind our goal: to assess the user's Dark Mode preference and apply it immediately. For this script to work, it must execute before the browser paints the page, so it has to block interactivity. This contradicts everything you've ever read about script optimization. Conventional guidance dictates scripts should run in an asynchronous, parallel fashion in order to maximize Web Vitals and get the user up and running as soon as possible. That general guidance is accurate, but we need to make an exception for this particular script. Still, it must execute very quickly, or we will lose customers.
Our strategy for implementing Dark Mode will factor in potential user preferences specific to the Vidya website set in
a key-value store available in modern browsers,
and/or system settings that the browser will inform us with
prefers-color-scheme. The algorithm goes like this:
If the user previously visited the Vidya website and indicated a preference for Dark Mode OR if there is no preference established and system settings are set for Dark Mode, then activate Dark Mode by attaching the dark class attribute to the root. Otherwise, apply Light Mode by removing any dark class attribute.
Here is the
darkMode.js script that does exactly that:
That's a straightforward conditional, which might even short-circuit, and DOM manipulation. That should be fast. Phew!
And here is how we execute it before browser paint with Next.js's
Script tag inside our
beforeInteractive strategy is the key. This tells Next.js to block everything until the script is finished. Again,
you need to use this strategy very carefully, but it's necessary and proper in this instance.
So thanks to Tailwind CSS and Next.js, we can successfully apply Dark Mode based on user preferences one way or another
when the Vidya website loads. The last step is to give the user a chance to switch modes and to save that preference to
With Great Effects Come Great Responsibility
When Facebook revolutionized React with Hooks, it was a game changer, but even now, years later, they can be confusing. Let's
see how we can use
useEffect to complete our Dark Mode solution.
The work we did with Tailwind CSS and the
Script tag presents our user interface exactly as it should look from what we know so far, but React needs to
manage that preference to change it as the user dictates. There are two steps:
- React needs to be made aware of the initial Dark Mode preference and keep an eye on it.
- If the user changes that preference, React needs to add or remove the
darkclass from the root and persist the choice in
These are two different effects. We will localize them where they matter most, the
ThemeButton the user clicks to switch modes.
Before we get into those, lets prepare to maintain state:
Although we really want
darkMode to be
false, we need to initialize it with
undefined because we don't know what
it is until the first effect runs.
Here it is:
It's simple but deceptively so. It's really very very sneaky.
Note the empty dependency array. Many React developers, especially the other old timers who remember the awkwardness of
handling effects in component lifecycle events, think of this as the equivalent of the initial set up we did in
That way of thinking can work for you, but it's imprecise and I would say counterproductive to understanding how React works.
The purpose of
useEffect is to synchronize UI with the state represented in the dependency array. When that state changes,
UI changes. However, the absence of dependencies means that you want to synchronize your UI with the absence of state,
and state just happens to be absent when a component first mounts. So yeah, it works out the same as that
analogy, but they're really two different things.
This is why math teachers make you show your work.
As a result, this first
useEffect call runs when state is absent as the component initially mounts, and the current
value is saved to state. We can deduce the value from the root element because of the code we wrote earlier using the Next.js
Script tag, which we know has already executed because we used the
See how it all fits together?
Finally, there is the second hook that triggers and records a change to the theme when the user clicks the button:
This is a more straightforward implementation of
darkMode state value is in the dependency array of the effect,
so when the user clicks the
ThemeButton and toggles the value with
setDarkMode, two effects execute. The code modifies the root
element by adding or removing the
dark class as needed and persists the setting to
localStorage so our
before will pick it up again when the user returns to the Vidya website.
Let's wrap up by putting together all the relevant Dark Mode logic in
So that's it. I hope it's clear how the different components of our solution complement one another to bring Dark Mode to the Vidya website, but this is just one way of doing it. I can't wait to see how you apply the lessons learned here to build great Dark Mode experiences for your audience as well. If you come up with a better way of doing it, please let us know.