How to implement a Dark Mode with Tailwind

Since recently Aureola has a dark mode. You can switch between the light and dark version of the page via the small sun in the upper right corner of the header. Your browser stores your selection. How this works exactly and how you can implement it yourself in Tailwind, I explain in this article.

Implementing a dark mode with Tailwind is basically not difficult. However, depending on how complex your design is, it can become a lot of work. There are two possible approaches here. Either you react with a media query directly in the CSS to the preferred color scheme of the user. Or you read the color scheme via Javascript and have the possibility to implement a toggle button. I will present you both options here and you can decide for yourself which one suits you better.

Define Colors as Variables

With Tailwind you have the option to customize the colors of your design via the configuration (see tailwind.config.js). Here you can either add colors or overwrite the color scheme completely. For example, you can specify colors as hexadecimal code. This looks like this:

module.exports = {
  theme: {
    colors: {
      'white': '#ffffff',
      'purple': '#3f3cbb',
      'midnight': '#121063',
      'metal': '#565584',
    },
  },
}

However, you also have the option to define colors as CSS variables. And this is exactly what we will use. The most common way is to define variables in the pseudo element :root. But in principle this is possible in every CSS selector. If we stick to the above example, it would look like this:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --color-white: #ffffff;
  --color-purple: #3f3cbb;
  --color-midnight: #121063;
  --color-metal: #565584;
}

And in the configuration we could use the variables like this:

module.exports = {
  theme: {
    colors: {
      'white': 'var(--color-white)',
      'purple': 'var(--color-purple)',
      'midnight': 'var(--color-midnight)',
      'metal': 'var(--color-metal)',
    },
  },
}

At this point it is important to know that we can also overwrite these variables. We can use this to add an alternative color scheme like this. In our case, this is of course a dark mode. But in principle, we could also use this to implement different themes. For example, a high-contrast mode for people who have problems with visual perception would also be conceivable.

Implement Dark Mode via CSS

A dark mode via CSS can be realized using a media query. The prefers-color-scheme property can now be used in almost all browsers. This allows us to react to the user's preference and then adjust our color variables. In a simple example it could look like this:

module.exports = {
  theme: {
    colors: {
      'light': 'var(--color-light)',
      'dark': 'var(--color-dark)',
    },
  },
}

We then define the variables in CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --color-light: #ffffff;
  --color-dark: #000000;
}

Via media query we then simply overwrite the previously defined variables:

@media (prefers-color-scheme: dark) {
  :root {
    --color-light: #000000;
    --color-dark: #ffffff;
  }
}

And that's it. When dark mode is activated, the colors are swapped. With more colors, of course, it's a little more complex. You may also want to preserve some colors. Of course you should not define those as variables or change them via media query.

Implement Dark Mode via Javascript

For various reasons, it can make sense to rely on Javascript instead of aiming for a pure CSS solution. For example, if you want to give users the opportunity to switch the color scheme via a button. Basically the procedure is quite similar to the previous variant. We define the colors as CSS variables in tailwind.config.js:

module.exports = {
  theme: {
    colors: {
      'light': 'var(--color-light)',
      'dark': 'var(--color-dark)',
    },
  },
}

We then define the variables in CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --color-light: #ffffff;
  --color-dark: #000000;
}

Now instead of proceeding with a media query here, we will introduce an additional class for the body and then use that to override the variables.

body.dark {
  --color-light: #000000;
  --color-dark: #ffffff;
}

We now create an additional Javascript file. In this we use matchMedia to determine if the user would prefer dark mode. This is basically a media query at the javascript level. The script could then look like this:

// Check if user prefers dark mode color scheme.
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

// If so, add the 'dark' class to the body element.
document.body.classList.toggle('dark', prefersDarkMode);

With this, we have reached our goal for the time being. However, this does not give us any advantages over the CSS variant. That is why we will now implement a toggle button. This will not only respect the user preference, but also allow switching the color scheme. In addition, we will remember the user's selection.

Button to Switch between Light & Dark Mode

First of all, we need to add a button to the page. For this tutorial, the appearance doesn't matter. All that matters is that it exists and is accessible to the user in some form. For example, the simplest form would be like this:

<button id="dark-mode-switch">
  Switch Color Scheme
</button>

The id is necessary so that we can address the button in Javascript. The implementation is of course also easily possible with Vue, Alpine or whatever. For this example I will only use Vanilla Javascript. A simple toggle could then be implemented like this:

document
    .getElementById('dark-mode-switch')
    .addEventListener('click', () => document.body.classList.toggle('dark'));

However, we also want to respect the user preferences, of course. In addition, we want to store the user's choice should he decide otherwise. We will implement the latter via Local Storage. We can then use this to create a function that determines whether we should use dark mode.

function useDarkMode() {
  // Check if we have stored the user preference using local storage.
  const colorScheme = window.localStorage.getItem('colorScheme');

  // If so, check if the color scheme should be dark. 
  if (colorScheme) {
    return colorScheme === 'dark';
  }
  
  // If not, use matchMedia to check the user's OS preferences.
  // This will return true, if the user wants to user dark mode.
  return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

We can then use the result of this function to set the desired color scheme. We will then always save the user selection here at the same time in any case.

function setDarkMode(enabled = false) {
  // Add or remove the 'dark' class.
  document.body.classList.toggle('dark', enabled);
  
  // Always store the user's preferences in local storage.
  window.localStorage.setItem('colorScheme', enabled ? 'dark' : 'light');
}

setDarkMode(useDarkMode());

In combination, we can now use the two functions together with our button to complete the desired feature.

let darkModeEnabled = false;

document
    .getElementById('dark-mode-switch')
    .addEventListener('click', () => setDarkMode(!darkModeEnabled));

function useDarkMode() {
    const colorScheme = window.localStorage.getItem('colorScheme');
    if (colorScheme) {
        return colorScheme === 'dark';
    }

    return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

function setDarkMode(enabled = false) {
    darkModeEnabled = enabled;
    document.body.classList.toggle('dark', enabled);
    window.localStorage.setItem('colorScheme', enabled ? 'dark' : 'light');
}

setDarkMode(useDarkMode());

As you can see, there are several ways to implement a dark mode with Tailwind. Which one do you prefer? Do you know another approach that could be interesting? Then feel free to share it in the comments. I say goodbye at this point. Farewell.

There are no comments yet.