Implementing Tailwind CSS Dark Mode Toggle with No Flicker
Tags: tailwindcss, nextjs, vue, webdev
Published: 2023-09-19 09:40:31

Dark layouts have become increasingly popular in interface designs, and we at Cruip are proud to have embraced this trend from the beginning. We’ve developed several best-seller [Tailwind CSS templates](https://cruip.com/?swcfpc=1) in dark skin, and in this tutorial, we will show you **how to add a dark functionality** to your layout bypassing one of the most common mistakes developers make: The flickering effect.

The **flickering effect** occurs when a user sets their preference to dark mode and navigates the site. The page initially loads in light mode but quickly switches to dark mode. This annoyance can be avoided through appropriate implementation.

Let’s see how we can add a dark layout toggle to our interfaces using **HTML**, **React**, and **Vue**.

Quick navigation

*   [HTML version](#HTML version)
*   [Nextjs version](#Nextjs version)
*   [Vue version](#Vue version)

Dark Mode implementation with HTML and JS only<a name="HTML version"></a>
----------------------------------------------

In this first part, I’ll show you how to integrate a Tailwind CSS dark mode toggle into a static website using only HTML and JavaScript.

Here are the steps we’ll follow:

*   Enable Dark Mode in the Tailwind CSS configuration file
*   Create an accessible toggle button using a checkbox
*   Determine the default theme based on the user’s operating system preferences (`prefers-color-scheme`)
*   Save the user’s preference in `localStorage` to remember their choice during navigation or subsequent visits
*   Prevent the annoying flickering effect during page loading, also known as FOUC (flash of unstyled content)

Let’s get started!

### Enabling Dark Mode in Tailwind CSS

If you’re familiar with Tailwind CSS, you might know that the framework allows enabling dark mode by simply adding `darkMode: 'class'` to the Tailwind configuration file.

```html
  /** @type {import('tailwindcss').Config} */
  module.exports = {
    darkMode: 'class',
    // ...
  }
```

Once enabled, you can use the `dark` class on the `html` tag (or any other element) to apply the dark mode styles to all children.

Whenever you use the `dark` class, Tailwind magically applies styles prefixed with `dark:` to override default styles.

### Creating a Dark Mode toggle switch

Now, let’s add a button that lets users switch between Light and Dark Mode. For this, we’re going with a checkbox input because it’s the most accessible and straightforward solution.

```html
  <input type="checkbox" name="light-switch" class="light-switch" />
  <label for="light-switch">Switch to light / dark version</label>
```

By default, the checkbox is disabled (representing the light theme). To make it look like a toggle button, we’ll use some Tailwind CSS classes:

```html
  <div class="flex flex-col justify-center ml-3">
      <input type="checkbox" name="light-switch" class="light-switch sr-only" />
      <label class="relative cursor-pointer p-2" for="light-switch">
          <svg class="dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
              <path class="fill-slate-300" d="M7 0h2v2H7zM12.88 1.637l1.414 1.415-1.415 1.413-1.413-1.414zM14 7h2v2h-2zM12.95 14.433l-1.414-1.413 1.413-1.415 1.415 1.414zM7 14h2v2H7zM2.98 14.364l-1.413-1.415 1.414-1.414 1.414 1.415zM0 7h2v2H0zM3.05 1.706 4.463 3.12 3.05 4.535 1.636 3.12z" />
              <path class="fill-slate-400" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
          </svg>
          <svg class="hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
              <path class="fill-slate-400" d="M6.2 1C3.2 1.8 1 4.6 1 7.9 1 11.8 4.2 15 8.1 15c3.3 0 6-2.2 6.9-5.2C9.7 11.2 4.8 6.3 6.2 1Z" />
              <path class="fill-slate-500" d="M12.5 5a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 5Z" />
          </svg>
          <span class="sr-only">Switch to light / dark version</span>
      </label>
  </div>
```

The toggle switch will look like this:

![The dark mode toggle button after adding Tailwind CSS classes and some SVGs](https://cruip.com/wp-content/uploads/2023/08/dark-mode-toggle-768x178.png)

Next up, let’s handle the JavaScript side of things. You can either create a separate .js file or add the code directly to your HTML just before the closing `body` tag.

```html
  <script>
  const lightSwitches = document.querySelectorAll('.light-switch');
  if (lightSwitches.length > 0) {
    lightSwitches.forEach((lightSwitch, i) => {
      if (localStorage.getItem('dark-mode') === 'true') {
        lightSwitch.checked = true;
      }
      lightSwitch.addEventListener('change', () => {
        const { checked } = lightSwitch;
        lightSwitches.forEach((el, n) => {
          if (n !== i) {
            el.checked = checked;
          }
        });
        if (lightSwitch.checked) {
          document.documentElement.classList.add('dark');
          localStorage.setItem('dark-mode', true);
        } else {
          document.documentElement.classList.remove('dark');
          localStorage.setItem('dark-mode', false);
        }
      });
    });
  }
  </script>
```

Let’s break down the above code to see how it works:

*   `const lightSwitches = document.querySelectorAll('.light-switch');` gets all elements with the class `light-switch` and stores them in a variable
*   `if (localStorage.getItem('dark-mode') === 'true') { lightSwitch.checked = true; }` checks if the user has previously opted for dark mode. If so, we make sure the checkbox input reflects that by setting it as checked
*   `lightSwitch.addEventListener('change', () => { ... });` adds an event listener to the checkbox input that triggers every time the user changes its state
*   `const { checked } = lightSwitch;` stores the checkbox input’s value in the `checked` variable
*   `lightSwitches.forEach((el, n) => { if (n !== i) { el.checked = checked; } });` ensures that all checkbox inputs synchronize with the input that triggered the event, maintaining a consistent state
*   The final code block adds or removes the `dark` class from the `html` element, based on the value of the checkbox input that triggered the event, and saves the user’s preference in `localStorage`

### Serving the right theme on page load

Here comes the crucial part: determining the theme to display when the page first loads. We’ve got a few scenarios to account for:

*   If it’s the user’s first visit, we’ll stick to the default light theme
*   If it’s their first visit, but dark mode is their system preference, we’ll serve the dark theme
*   For returning visitors who’ve toggled before, we’ll keep things consistent by displaying the theme they previously chose, saved in `localStorage`

To make this happen, we’ll add some more JavaScript. This time, we’ll place it within the `head` tag of our HTML file. By doing this, our JavaScript will run before the page loads, allowing us to avoid the unwanted flickering effect.

```javascript
  <script>
  if (localStorage.getItem('dark-mode') === 'true' || (!('dark-mode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.querySelector('html').classList.add('dark');
  } else {
      document.querySelector('html').classList.remove('dark');
  }
  </script>
```

With this step, we’re all set. In the upcoming sections, I’ll show you how to integrate the Tailwind CSS dark mode toggle into dynamic sites using Next.js and Vue.

Dark Mode implementation with Next.js<a name="Nextjs version"></a>
-------------------------------------

JavaScript frameworks like Next.js and Vue require different strategies for the dark mode implementation. Our earlier solution won’t work.

Let’s start with Next.js. We’ll use the [next-themes](https://github.com/pacocoursey/next-themes) package, which allows us to handle dark mode very easily without worrying about blocking page rendering to check user preferences.

Of course, we must wnsure to enable dark mode in Tailwind CSS, as we saw earlier. After that, we’ll install next-themes with the terminal command `npm i next-themes --save`.

Once that’s done, we need a component that lets users toggle dark mode on and off. For this, create a new file named `theme-toggle.tsx` and inject the following code with the [checkbox input](#creating-a-dark-mode-toggle-switch) we created earlier:

```tsx
  'use client'
  
  import { useTheme } from 'next-themes'
  
  export default function ThemeToggle() {
  
    const { theme, setTheme } = useTheme()
  
    return (
      <div className="flex flex-col justify-center ml-3">
        <input
          type="checkbox"
          name="light-switch"
          className="light-switch sr-only"
          checked={theme === 'light'}
          onChange={() => {
            if (theme === 'dark') {
              return setTheme('light')
            }
            return setTheme('dark')
          }}
        />
        <label className="relative cursor-pointer p-2" htmlFor="light-switch">
          <svg className="dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
            <path
              className="fill-slate-300"
              d="M7 0h2v2H7zM12.88 1.637l1.414 1.415-1.415 1.413-1.413-1.414zM14 7h2v2h-2zM12.95 14.433l-1.414-1.413 1.413-1.415 1.415 1.414zM7 14h2v2H7zM2.98 14.364l-1.413-1.415 1.414-1.414 1.414 1.415zM0 7h2v2H0zM3.05 1.706 4.463 3.12 3.05 4.535 1.636 3.12z"
            />
            <path className="fill-slate-400" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
          </svg>
          <svg className="hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
            <path className="fill-slate-400" d="M6.2 1C3.2 1.8 1 4.6 1 7.9 1 11.8 4.2 15 8.1 15c3.3 0 6-2.2 6.9-5.2C9.7 11.2 4.8 6.3 6.2 1Z" />
            <path
              className="fill-slate-500"
              d="M12.5 5a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 5Z"
            />
          </svg>
          <span className="sr-only">Switch to light / dark version</span>
        </label>
      </div>
    )
  }
```

There’s not much to explain, really. We’ve simply imported `useTheme` from next-themes and used the hook to get the current theme and set the theme. Easy, right?

Now, however, we need to add and remove the `dark` class from the `html` element based on the current theme. To do this, create a new file named `theme-provider.tsx` and add this code:

```tsx
  'use client'
  
  import { ThemeProvider } from 'next-themes'
  
  export default function Theme({ children }: { children: React.ReactNode }) {
    return (
      <ThemeProvider attribute="class">
        {children}
      </ThemeProvider>
    )
  }
```

We’re not quite done yet. Since we’re in the Next.js 13 era with the `app` directory, let’s get into the `layout.tsx` file and include the theme provider we created earlier:

```tsx
  import Theme from './theme-provider'
  
  export default function Layout({ children }) {
    return (
      <html suppressHydrationWarning>
        <body>
          <Theme>{children}</Theme>
        </body>
      </html>
    )
  }
```

Note that we added `suppressHydrationWarning` to the `html` tag to prevent Next.js from showing a warning in the console.

Dark Mode implementation with Vue<a name="Vue version"></a>
---------------------------------

Now, let’s talk about Vue! Once again, we’ll use an external library to handle dark mode simply and quickly. In this case, the library is called [VueUse](https://vueuse.org/), and you can install it using the command `npm i @vueuse/core --save`.

After that, create a new file named `ThemeToggle.vue`, and add this code:

```html
  <template>
    <div>
      <input type="checkbox" name="light-switch" v-model="isDark" class="light-switch sr-only" />
      <label class="flex items-center justify-center cursor-pointer w-8 h-8 bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600/80 rounded-full" for="light-switch">
        <svg class="w-4 h-4 dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
          <path class="fill-current text-slate-400" d="M7 0h2v2H7V0Zm5.88 1.637 1.414 1.415-1.415 1.413-1.414-1.414 1.415-1.414ZM14 7h2v2h-2V7Zm-1.05 7.433-1.415-1.414 1.414-1.414 1.415 1.413-1.414 1.415ZM7 14h2v2H7v-2Zm-4.02.363L1.566 12.95l1.415-1.414 1.414 1.415-1.415 1.413ZM0 7h2v2H0V7Zm3.05-5.293L4.465 3.12 3.05 4.535 1.636 3.121 3.05 1.707Z" />
          <path class="fill-current text-slate-500" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
        </svg>
        <svg class="w-4 h-4 hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
          <path class="fill-current text-slate-400" d="M6.2 2C3.2 2.8 1 5.6 1 8.9 1 12.8 4.2 16 8.1 16c3.3 0 6-2.2 6.9-5.2C9.7 12.2 4.8 7.3 6.2 2Z" />
          <path class="fill-current text-slate-500" d="M12.5 6a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 6Z" />
        </svg>
        <span class="sr-only">Switch to light / dark version</span>
      </label>
    </div>
  </template>
  
  <script setup>
  import { useDark } from "@vueuse/core";
  const isDark = useDark({
    selector: 'html',
  })
  </script>
```

Notice how we used `useDark` from VueUse to get the current theme and set the theme. We also used `v-model` to bind the checkbox input value to the current theme.

Unlike Next.js, in Vue, we don’t need a provider to handle dark mode! We can simply use `useDark` in any component, and it will work. Simple as pie, right?

Conclusions
-----------

If you want to see how we at Cruip have previously implemented this technique, take a look at one of our templates below:

*   **Mosaic** – [Tailwind CSS admin dashboard template](https://cruip.com/mosaic/?swcfpc=1)
*   **Stellar** – [Dark Next.js landing page template](https://cruip.com/stellar/?swcfpc=1)
*   **Quoty** – [Quote and invoice Tailwind CSS template](https://cruip.com/quoty/?swcfpc=1)
*   **Appy** – [Mobile web design template](https://cruip.com/appy/?swcfpc=1)
*   **DevSpace** – [Tailwind blog template](https://cruip.com/devspace/?swcfpc=1)
*   **Docs** – [Documentation website template](https://cruip.com/docs-template/?swcfpc=1)

There are a lot of them, but as we’d told you before, we’re big fans of dark layouts 🙂