This article was originally published on my personal website here. There's some other react/typescript related content there as well if you want to have a look.
Creating a Dark Theme with Tailwind in Nextjs
Author: Daniel Einars
Date Published: 30.10.2022
1. Intro
With nextjs becoming the gold standard for developing react applications I thought I'd briefly explain how to create a
nice dark theme using nextjs and tailwind. We're going build the following
- Theme Toggler
- Theme Context Provider
- Example Usage
By the end of it you'll be able to
Prerequisites:
- A running nextjs application with tailwind configured.
- Following dependencies:
-
js-cookie@^3.0.1
&@types/js-cookie@^3.0.2
to persist the theme -
tailwindcss@3.0.5
for styling the page
-
2. Theme Context Provider
Before we start. Tailwind implements their dark theme by css child selectors. Basically, if you're HTML
element
has class="dark"
, them it will automatically apply all dark:some-tailwind-class
styles. That's why all the
functionallity around this will involve adding/removing class="dark"
when toggeling a theme.
We'll be using createContext
in order to keep track of the theme and witch it when we want. In this instance.
But why are we using react's context? It causes a lot of rerenders and.. stuff!
.. to which I say:
"nu-uh! react's context is a dependency injection tool and if we don't bastardise it's intended
usage we're not causing any harm!"
For everyone who just wants to copy&paste everything just scroll to the bottom for the completed work.
2.1. Creating the Context.
This is fairly straight forward, so I won't dive into any details.
const initialState = false; // we start the first-time visitors up on the light theme export const ThemeContext = createContext({ isDarkTheme: initialState, // pass in the inital state toggleThemeHandler: () => { }, // define a function to toggle the theme });
2.2. The Theme Context Provider
The Context Provider has to handle the following two cases.
- New user:
- Default to light theme
- Set light theme cookie
- Set
class="light"
on theHTML
element
- Returning user:
- Read cookie
- Call
setIsDarkTheme
with appropriate value - Set
class="light"
orclass="dark"
on theHTML
element (technically we don't need thelight
class, but I like to keep it there)
- Change the theme when the user wants to
We're going to need two functions. One to initialize the theme and handle cases 1
and 2
. And a function to actually
toggle the theme.
2.2.1. Initializer
The ThemeContextProvider
keeps track of which theme we currently have enabled in its own useState call. You could rely
on the cookie exclusively, but I found that to be a bit of a pain. Hence, we
have const [isDarkTheme, setIsDarkTheme] = useState(initialState);
at the very top. This way we can also
call initialThemeHandler
whenever the user decided to change the theme and we don't have to separate
a initialThemeHandler
function from a setTheme
function.
const [isDarkTheme, setIsDarkTheme] = useState(initialState); const initialThemeHandler = useCallback((): void => { // Get current cookie theme. const themeCookie = Cookies.get("theme"); // js-cookie returns `undefined` if there's no cookie by that name setIsDarkTheme(themeCookie === "dark"); // Here we start handling the case when we have a returning user (because we found a cookie) if (themeCookie) { // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); // The cookie will have the value of `dark` or `light` // Therefore we can just set it to the value of the cookie document.querySelector("html")?.classList.add(themeCookie); } else { // Oooo!! A new user! // I set the cookie expiration to 30 days, but that's optional const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the default light cookie Cookies.set("theme", "light", { secure: true, expires: expires, }); } // we're going to call this callback everytime the `isDarkTheme` property changes }, [isDarkTheme]); // an dalso on the initial render useEffect(() => initialThemeHandler(), [initialThemeHandler]);
2.2.2. Theme toggle function
This is the function which the context will provide to other components via (say it with me) dependency injection.
function toggleThemeHandler(): void { // get the current theme cookie. We know it exists since this will 100% of the time run after the `initialThemeHandler` function const themeCookie = Cookies.get("theme"); // What ever theme we previously had, set it to the opposite. // Remember this is a boolean! setIsDarkTheme((ps) => !ps); // Create a new cookie expiration date const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the cookie to the opposit of what ever it's currently holding Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", { secure: true, expires: expires, }); // add the appropriate class to the `HTML element toggleDarkClassToHTMLElement(); }
All this function does is remove either dark
or light
and then add either dark
or light
to the HTML
element.
I chose this element because it's the top most one and I ran into some issues with nextjs and changing the body
classes.
function toggleDarkClassToHTMLElement(): void { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light"); }
Lastly we return the ThemeContext.Provider
like this
return ( <ThemeContext.Provider value={ { isDarkTheme, // remember, this is a boolean toggleThemeHandler // handler to toggle the theme }}> {props.children} // all other components will be child components of this one </ThemeContext.Provider> );
Next we need to initialize the theme. That should handle the following scenarios
const initialThemeHandler = useCallback((): void => { // Get current cookie theme. const themeCookie = Cookies.get("theme"); // js-cookie returns `undefined` if there's no cookie by that name setIsDarkTheme(themeCookie === "dark"); // Here we start handling the case when we have a returning user (because we found a cookie) if (themeCookie) { // Just to be super-duper sure we're adding the right classes, remove what ever the other theme is document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); // The cookie will have the value of `dark` or `light` // Therefore we can just set it to the value of the cookie document.querySelector("html")?.classList.add(themeCookie); } else { // Oooo!! A new user! // I set the cookie expiration to 30 days, but that's optional const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); // Set the default light cookie Cookies.set("theme", "light", { secure: true, expires: expires, }); } // we're going to call this callback everytime the `isDarkTheme` property changes }, [isDarkTheme]); // an dalso on the initial render useEffect(() => initialThemeHandler(), [initialThemeHandler]);
3. Applying themes
Since we want all our components to be able to access the current theme and toggle it, we're going to wrap our entire
nextjs app in the provider. To do this we create a _app.tsx
file and wrap all components in the provider like this
export default function Root({Component, pageProps}: AppProps) { return ( <ThemeContextProvider> <Component {...pageProps} /> </ThemeContextProvider> ); }
For now, we'll be using an old-fashioned button to toggle the theme.
interface IThemeTogglerContext{ isDarkTheme: boolean; toggleThemeHandler: () => void; } export function ThemeToggleButton(){ // get the `toggleThemeHandler` via *dependency injection* const { toggleThemeHandler }: IThemeTogglerContext = useContext(ThemeContext); // toggle the theme onClick function toggle(){ toggleThemeHandler() } // super fancy button return ( <button onClick class={classNames( // styles which won't be affected by the theme "font-bold py-2 px-4 rounded-full", // light theme styles "bg-blue-500 hover:bg-blue-700 text-white", // dark theme styles "dark:bg-blue-100 dark:hover:bg-blue-200 dark:text-red-50", )}> Toggle Theme </button>) }
You can then place this button anywhere you want and it'll update the theme. As mentionind in the beginning, tailwind applies the dark-theme using CSS selectors, so any styles you want in a dark theme, just prefix the selector with a dark:
prefix, as it's d
4. Styling the Body and adding theme switching transition
Because I want to see some sort of small transition when I change themes, I also created a _document.tsx
file and added some tailwind classes, which make the theme switching a pleasent experience. Here's the completed work
import Document, { Head, Html, Main, NextScript } from "next/document"; export default class _Document extends Document { render() { return ( <Html> <Head> <title>dle.dev</title> </Head> <body className="bg-neutral-50 dark:bg-neutral-900 transition-colors overflow-x-hidden "> <Main /> <NextScript /> </body> </Html> ); } }
Yes, I know my Head
/ Title
config isn't best practice. Check out what vercel is saying on the subject here.
5. Entire Snippit
There you go, that's all it took! In case you want to try it out, here's the complete ThemeContext
component:
import type { ReactElement, ReactNode } from "react"; import { createContext, useCallback, useEffect, useState } from "react"; import Cookies from "js-cookie"; const initialState = false; export const ThemeContext = createContext({ isDarkTheme: initialState, toggleThemeHandler: () => {}, }); interface ThemePropsInterface { children: ReactNode; } export function ThemeContextProvider(props: ThemePropsInterface): ReactElement { const [isDarkTheme, setIsDarkTheme] = useState(initialState); const initialThemeHandler = useCallback((): void => { const themeCookie = Cookies.get("theme"); setIsDarkTheme(themeCookie === "dark"); if (themeCookie) { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(themeCookie); } else { const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); Cookies.set("theme", "light", { secure: true, expires: expires, }); } }, [isDarkTheme]); useEffect(() => initialThemeHandler(), [initialThemeHandler]); function toggleThemeHandler(): void { const themeCookie = Cookies.get("theme"); setIsDarkTheme((ps) => !ps); const date = new Date(); const expires = new Date(date.setMonth(date.getMonth() + 1)); Cookies.set("theme", themeCookie !== "dark" ? "dark" : "light", { secure: true, expires: expires, }); toggleDarkClassToBody(); } function toggleDarkClassToBody(): void { document.querySelector("html")?.classList.remove(isDarkTheme ? "dark" : "light"); document.querySelector("html")?.classList.add(!isDarkTheme ? "dark" : "light"); } return ( <ThemeContext.Provider value={{ isDarkTheme, toggleThemeHandler }}> {props.children} </ThemeContext.Provider> ); }
Top comments (1)
Thank you for sharing this..