{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "@id": "https://www.vidyasource.com/blog/dark-mode-nextjs-tailwindcss-react-hooks/",
  "url": "https://www.vidyasource.com/blog/dark-mode-nextjs-tailwindcss-react-hooks/",
  "mainEntityOfPage": "https://www.vidyasource.com/blog/dark-mode-nextjs-tailwindcss-react-hooks/",
  "headline": "Dark Mode in Next.js using Tailwind CSS and React Hooks",
  "description": "Use the power of Tailwind CSS and React Hooks to build Dark Mode users can control into your Next.js site.",
  "datePublished": "2021-08-02T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "Neil Chaudhuri",
    "jobTitle": "President",
    "url": "https://www.linkedin.com/in/neil-chaudhuri/",
    "sameAs": "https://www.linkedin.com/in/neil-chaudhuri/"
  },
  "publisher": {
    "@id": "https://www.vidyasource.com/#organization"
  },
  "image": "https://www.vidyasource.com/img/blog/moon.jpg",
  "keywords": [
    "NextJS",
    "Tailwind CSS",
    "JavaScript",
    "TypeScript",
    "React",
    "Mobile",
    "Accessibility",
    "Open Source"
  ],
  "articleBody": "It's quite possible that while waiting for the ads on Hulu to end\nyou stumbled upon the [option to set your phone's theme to Dark Mode](https://www.theverge.com/2019/3/22/18270975/how-to-dark-mode-iphone-android-mac-windows-xbox-ps4-nintendo-switch).\nDark Mode is becoming a staple of user interfaces on the web and mobile devices for [several reasons](https://www.forbes.com/uk/advisor/mobile-phones/what-is-dark-mode-and-should-you-be-using-it/)--\nprimarily to ease the strain on your eyes and to reduce battery consumption.\n\nAt Vidya we pride ourselves on embracing emerging technologies and helping our clients leverage them to realize their\npotential. 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\nthat mission. This website you're reading right now supports Dark Mode. Just look at the top of the page.\n\nThe site is built in [TypeScript](https://www.typescriptlang.org/) with [React](https://reactjs.org/), the most popular JavaScript library in the world, using [Next.js](https://nextjs.org/), one\nof the most popular React frameworks in the world and the building block for full-stack \"meta\" frameworks like [RedwoodJS](https://redwoodjs.com/) and\n[Blitz](https://blitzjs.com/). The user interface itself is crafted with the ever popular [Tailwind CSS](https://tailwindcss.com/),\na powerful \"utility-first\" library that lets you compose your styles into higher-level abstractions that you apply across your user interface\nto give a consistent look and feel.\n\nIf you would like to implement Dark Mode on a Next.js site using TailwindCSS, let me show you how. It involves three key pieces:\n\n* Tailwind's `dark` class\n* The `Script` tag that we got in Next.js 11\n* Understanding, like really understanding, React's `useEffect` hook\n\n## Activating Tailwind's Dark Mode Support\n\nTailwind CSS offers [two ways to set Dark Mode](https://tailwindcss.com/docs/dark-mode). If you are content to default to system settings, then all\nyou need to do is confirm your `tailwind.config.js` file has the `media` setting, which uses the `prefers-color-scheme` [CSS media feature](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme):\n\n~~~js\n// tailwind.config.js\nmodule.exports = {\n  darkMode: 'media',\n}\n~~~\n\nBut since we want more control to let Vidya users decide which look they prefer, we need the `class` setting instead:\n\n~~~js\n// tailwind.config.js\nmodule.exports = {\n  darkMode: 'class',\n}\n~~~\n\nNow you need to handle variants like [the TVA in Loki](https://www.gamesradar.com/loki-lady-loki-kid-loki-king-loki-disney-plus/).\n[Variants in Tailwind](https://tailwindcss.com/docs/configuring-variants) define the ways in which you want to apply different styles.\nFor example, if we want to set a red background on a link hover, we apply the `hover` variant on the `bg` plugin: `<a className=\"hover:bg-red\">`.\n\nAs an aside, the CSS equivalent would be this for our shade of red:\n\n~~~css\na:hover {\n  background-color: #9C4D61;\n}\n~~~\n\nWe will do similar to apply `dark` variants of our branding scheme throughout our interface. For example, here is a simplified\nversion of our `contact-us` class composing numerous Tailwind utilities in Next.js's `globals.css` file:\n\n~~~css\n.contact-us {\n        @apply dark:text-red dark:hover:text-blue bg-red dark:bg-red-light hover:bg-blue-dark dark:hover:bg-blue-light;\n}\n~~~\n\nNote that you always put `dark` first when you have multiple variants like `dark:hover:bg-blue-light`.\n\nThis is where you will spend most of your time. Mostly because you want to put together a Dark Mode color palette that is usable\nand accessible and consistent with your branding and because you want to be thorough in applying it throughout the site.\n\nJust remember to [extract components](https://tailwindcss.com/docs/extracting-components) as we did above to keep things maintainable, consistent, and organized.\n\nBecause 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\nroot element of each page like this:\n\n~~~html\n<html lang=\"en\" class=\"dark\">\n...\n</html>\n~~~\n\nAnd we need to be able to do it on demand. This is where our code comes into play.\n\n## The Script Tag\n\nIf you've built a website with a lot of client side business functionality, GDPR or other consent management, Google Analytics, social media, or ads, you already know that managing JavaScript\nexecution 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 `head` element\nor at the bottom of the `body` element? It's actually easier figuring out where to seat everyone at your wedding.\n\nIn v11.0.0, Next.js introduced the `Script` [tag](https://nextjs.org/docs/basic-features/script), and it makes all this\na lot better. You can put the `Script` tag anywhere, and you apply one of three strategies to let Next.js know when it\nshould execute.\n\nBefore 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,\nit must execute *before* the browser paints the page, so it has to block interactivity. This\ncontradicts everything you've ever read about script optimization. Conventional guidance dictates scripts should run in an\nasynchronous, parallel fashion in order to maximize [Web Vitals](https://web.dev/vitals/) and get the user up and running as soon\nas possible. That general guidance is accurate, but we need to make an exception for this particular script. Still, it must\nexecute very quickly, or we will lose customers.\n\nOur strategy for implementing Dark Mode will factor in potential user preferences specific to the Vidya website set in `localStorage`,\na [key-value store available in modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage),\nand/or system settings that the browser will inform us with `prefers-color-scheme`. The algorithm goes like this:\n\n*If the user previously visited the Vidya website and indicated a preference for Dark Mode OR if there is no preference established\nand system settings are set for Dark Mode, then activate Dark Mode by attaching the dark class attribute to the root. Otherwise,\napply Light Mode by removing any dark class attribute.*\n\nHere is the `darkMode.js` script that does exactly that:\n\n~~~js\nif (localStorage.getItem('vidyaDarkMode') === 'true' || (!('vidyaDarkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {\n    document.documentElement.classList.add('dark')\n} else {\n    document.documentElement.classList.remove('dark')\n}\n~~~\n\nThat's a straightforward conditional, which might even [short-circuit](https://medium.com/@amaliesmidth/javascript-short-circuit-conditionals-6606bdeaa30d), and DOM manipulation. That should be fast. Phew!\n\nAnd here is how we execute it before browser paint with Next.js's `Script` tag inside our `_app.tsx`:\n\n~~~js\nimport Script from \"next/script\";\n// ...\n<Script strategy=\"beforeInteractive\" src=\"/scripts/darkMode.js\"/>\n~~~\n\nThe `beforeInteractive` strategy is the key. This tells Next.js to block everything until the script is finished. Again,\nyou need to use this strategy very carefully, but it's [necessary and proper](https://constitution.congress.gov/browse/essay/artI_S8_C18_1/#:~:text=C18.-,1%20The%20Necessary%20and%20Proper%20Clause%3A%20Overview,%2C%20Section%208%2C%20Clause%2018%3A&text=To%20make%20all%20Laws%20which,any%20Department%20or%20Officer%20thereof.) in this instance.\n\nSo thanks to Tailwind CSS and Next.js, we can successfully apply Dark Mode based on user preferences one way or another\nwhen the Vidya website loads. The last step is to give the user a chance to switch modes and to save that preference to `localStorage`.\n\n## With Great Effects Come Great Responsibility\n\nWhen Facebook revolutionized React with Hooks, it was a game changer, but even now, years later, they can be confusing. Let's\nsee how we can use `useState` and `useEffect` to complete our Dark Mode solution.\n\nThe 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\nmanage that preference to change it as the user dictates. There are two steps:\n\n* React needs to be made aware of the initial Dark Mode preference and keep an eye on it.\n* If the user changes that preference, React needs to add or remove the `dark` class from the root and persist the choice in `localStorage` accordingly.\n\nThese are two different effects. We will localize them where they matter most, the `ThemeButton` the user clicks to switch modes.\n\nBefore we get into those, lets prepare to maintain state:\n\n~~~js\nconst [darkMode, setDarkMode] = useState<boolean | undefined>(undefined)\n~~~\n\nAlthough we really want `darkMode` to be `true` or `false`, we need to initialize it with `undefined` because we don't know what\nit is until the first effect runs.\n\nHere it is:\n\n~~~js\nuseEffect(() => {\n        setDarkMode(document.documentElement.classList.contains(\"dark\"))\n}, [])\n~~~\n\nIt's simple but deceptively so. It's really [very very sneaky](https://www.youtube.com/watch?v=ESrtX53Kc8Q).\n\nNote the empty dependency array. Many React developers, especially the other old timers who remember the awkwardness of\nhandling effects in component lifecycle events, think of this as the equivalent of the initial set up we did in `componentDidMount`.\nThat way of thinking can work for you, but it's imprecise and I would say counterproductive to understanding how React works.\n\nThe purpose of `useEffect` is to synchronize UI with the state represented in the dependency array. When that state changes,\nUI changes. However, the *absence of dependencies* means that you want to synchronize your UI with the *absence of state*,\nand state just happens to be absent when a component first mounts. So yeah, it works out the same as that `componentDidMount`\nanalogy, but they're really two different things.\n\nThis is why math teachers make you show your work.\n\nAs a result, this first `useEffect` call runs when state is absent as the component initially mounts, and the current `darkMode`\nvalue is saved to state. We can deduce the value from the root element because of the code we wrote earlier using the Next.js\n`Script` tag, which we know has already executed because we used the `beforeInteractive` strategy.\n\nSee how it all fits together?\n\nFinally, there is the second hook that triggers and records a change to the theme when the user clicks the button:\n\n~~~js\nuseEffect(() => {\n        if (darkMode) {\n            window.document.documentElement.classList.add('dark')\n            localStorage.setItem(\"vidyaDarkMode\", \"true\")\n        } else {\n            window.document.documentElement.classList.remove('dark')\n            localStorage.setItem(\"vidyaDarkMode\", \"false\")\n        }\n}, [darkMode])\n\nconst onClick = () => {\n        setDarkMode(!darkMode)\n}\n~~~\n\nThis is a more straightforward implementation of `useEffect`. The `darkMode` state value is in the dependency array of the effect,\nso when the user clicks the `ThemeButton` and toggles the value with `setDarkMode`, two effects execute. The code modifies the root\nelement by adding or removing the `dark` class as needed and persists the setting to `localStorage` so our `Script` from\nbefore will pick it up again when the user returns to the Vidya website.\n\nLet's wrap up by putting together all the relevant Dark Mode logic in `ThemeButton` :\n\n~~~js\nexport const ThemeButton = (p: ThemeButtonProps) => {\n    const [darkMode, setDarkMode] = useState<boolean | undefined>(undefined)\n    useEffect(() => {\n        setDarkMode(document.documentElement.classList.contains(\"dark\"))\n    }, [])\n    useEffect(() => {\n        if (darkMode) {\n            window.document.documentElement.classList.add('dark')\n            localStorage.setItem(\"vidyaDarkMode\", \"true\")\n        } else {\n            window.document.documentElement.classList.remove('dark')\n            localStorage.setItem(\"vidyaDarkMode\", \"false\")\n        }\n    }, [darkMode])\n    const onClick = () => {\n        setDarkMode(!darkMode)\n    }\n\n    return ( {/* ThemeButton UI goes here */} )\n}\n~~~\n\nSo 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\njust 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\naudience as well. If you come up with a better way of doing it, please [let us know](https://twitter.com/VidyaSource)."
}