Dark mode seems to be all the rage right now, and there are plenty of articles explaining how to achieve this in Next.js already, but what if you want to allow additional color schemes beyond dark and light mode?
And for Tailwind users, how can this work with its dark mode feature?
Tailwind’s Dark Mode
Before we go over how to implement something like this, let's quickly review Tailwind’s Dark Mode feature. Tailwind ships a dark:
class variant, which leverages the prefers-color-scheme
media feature by default, but it can also be toggled manually with a class instead with this tailwind config setting:
From Tailwind's dark mode docs:
“Now instead of dark:{class}
classes being applied based on prefers-color-scheme
, they will be applied whenever dark
class is present earlier in the HTML tree.”
This approach will lend itself perfectly to building toggle switches in our Next.js application (more on that later), allowing users to switch between modes rather than solely relying on their preferred color scheme.
But what if we want to offer additional color schemes beyond dark mode?
Extending Tailwind
It’s actually easier than you'd think to add additional class-based color schemes to Tailwind, beyond their default dark
mode.
In the tailwind.config.js
file this is all we need:
The above custom plugin adds a metal:
class variant we can use to target our new color scheme! Just like with the dark:
class variant, metal:{class}
classes will be applied whenever the metal
class is present earlier in the HTML tree.
Custom utility classes
One of the first things you might notice is that once you start incorporating more color modes, adjusting backgrounds and text colors for your components will become a bit cumbersome.
Consider the following example:
All of that is just to define some colors on a card component!
Now imagine that mixed with a dozen other classes 🥴
And what if you have this same color setup across multiple components and then decide you actually need to change one of the colors defined for a specific color scheme or need to add another color scheme down the line? 😵💫
Sounds like hell, right?
Enter: ✨custom utility classes✨
Here's where we can leverage defining our own utility classes to both slim down these class definitions in our components, and make it immensely easier to manage going forward.
Personally, I like to do this in a custom _utilities.css
file, instead of inside the tailwind.config.js
file. Let's see how that file is setup:
In this file, we can start defining custom classes to use in our components, a perfect place to combine our lengthy color classes for all of our different modes. Here's how I like to do it:
A note on @apply
I know there’s a lot of strong feelings about the use of @apply
. But even Adam himself made a great example of when using @apply
is acceptable and reasonable in this tweet.
Using the above, let’s see how our fake <Card />
component looks now:
Kinda nice right? Our class list is now much easier to scan, and if we need to adjust a color scheme (or add more!) we only have to do it in one place, instead of across every component that has color mode-specific styles!
Setting Themes in Next.js
Now that we have a clean way to set up different color schemes with our CSS, we need to know how to switch between them on our frontend since we're not relying on the user's system preferences.
To help with this we'll use the next-themes package, which handles persisting the user's selected them, adding/removing class names, and provides a hook to get and set the current theme.
First, we need to set up a custom _app.js
file to wrap our app in the <ThemeProvider />
component required by next-themes, but with a few custom configurations in place:
Settings Explained
themes={['dark', 'light', 'metal']}
(required)
Light and Dark are included by default, so we need to explicitly define this to include our additional themes.attribute="class"
(required)
Since our Tailwind config is basing color schemes on the presence of a class earlier in the HTML tree, this must be set toclass
as well.defaultTheme="dark"
(optional)
The default theme is normally light, but if you prefer a different default theme for first-time visitors, you can explicitly set it.disableTransitionOnChange
(optional)
This ensures your UI with different transition durations won't feel inconsistent when changing the theme.enableSystem={false}
(optional)
Since I have a preferred color scheme for my first-time visitors, I don't want it to switch automatically between light/dark based on the user's system preferences, but you might!
With the above, our application will default to dark
mode using the class
attribute on the <html>
element.
Building a <ThemeSwitcher />
component
Now that we have our themes wired up in both our Tailwind classes and our Next.js application, let's build a component that will display the currently selected theme and allow users to cycle through our available themes.
Here's an interactive example of what we're building. Try it out:
The markup
Luckily, next-themes comes with a handy hook called useTheme()
that returns the current theme and a method to change it. Let's set up our component with this and display our current theme:
Uh-oh! If we include this on our pages as-is, we'll get this error:
Since the server doesn't know the theme, the value returned from useTheme()
will be undefined
until mounted on the client, causing the mismatch.
Mounting Client-side
We can fix this by only mounting this component client-side with the help of a handy useHasMounted()
hook from the lovely Josh Comeau:
Incorporating that into our component we can fix the hydration errors and see the current value when the page loads:
Setting a theme
We can also use the setTheme()
method to switch to any one of our other defined themes. Let's add a button to handle switching to a specific theme:
With the above, clicking our button will switch the current theme to metal
: updating the current theme being returned from our useTheme()
hook and applying the appropriate class to our HTML tree.
At this point, we've got everything we need to go beyond dark mode 🥳
BONUS: Cycle through themes on click
Rather than manually setting up buttons to switch to specific themes, let's actually make one button that when clicked cycles through our list of themes.
To do this, we'll first define our themes in an array outside of our component. I like to put these in a static data file, but you can also toss this directly in the component file if you'd prefer:
The benefit of this is we can control the order of our themes in the cycle, and extend information about each theme if needed for things like display titles or accessible labels.
By using the theme
returned from our hook, we can find the matched object in our new themes
array and use it's index to grab the next object (theme) in the array:
How does it loop?
This is because of % themes.length
: The remainder operator gives you the remaining value of dividing the first value with the second. So, in our example 2 + 1 % 3
equals zero, bringing us back to our first index in the array.
In Summary
Not everything is black and white, and the same can be said about color schemes! With the help of next-themes and some useful Tailwind configurations, you can offer color schemes beyond dark mode.