Tailwind Dark Mode

Jan 31, 2021 at 15:50

Adding a dark mode toggle to your site using Tailwind is fairly straightforward.

Firstly, we ensure that dark mode is enabled, and also enabled for display elements, by editing our tailwind.config.js:

module.exports = {
  darkMode: 'class', // or 'media' or 'class'
  variants: {
    extend: {
      display: ['dark']
    },
  },
};

Then we create two buttons, one to toggle to light mode, and the other to dark:

<div>
    <button class="colour-mode__btn hidden dark:block focus:outline-none">
        <span><!-- sun.svg --></span>
    </button>
    <button class="colour-mode__btn dark:hidden focus:outline-none">
        <span><!-- moon.svg --></span>
    </button>
</div>

You can get a sun and moon svg from the heroicons site and just paste them inline.

In the html we are doing a few things:

  • tagging the buttons with the empty class colour-mode__btn (that’s BEM naming).
  • leveraging Tailwind’s dark mode to hide the Sun button in light mode, and hide the Moon button in dark mode.

Finally, we need a tiny bit of javascript:

var toggleColourMode = function toggleColourMode(e) {
  if (e.currentTarget.classList.contains("dark:hidden")) {
    document.documentElement.classList.add('dark')
    localStorage.setItem("theme", "dark");
    return;
  }
  document.documentElement.classList.remove('dark')
  localStorage.setItem("theme", "light");
};

// Export if you're using this in a build.
/* export */ function buttonColourMode() {
  var toggleButtons = document.querySelectorAll(".colour-mode__btn");
  toggleButtons.forEach(function(btn) {
    btn.addEventListener("click", toggleColourMode);
  });
  if (localStorage.getItem("theme") === "dark") { document.documentElement.classList.add('dark'); };
}

We use the empty class colour-mode__btn to identify the two toggle buttons, then add a click handler. The click handler then checks to see if the element is either the dark mode button (has dark:hidden in the classList), or not, and sets the document class and storage appropriately.

Finally call this method when the document loads, e.g.

const load = () => {
  buttonColourMode();
};

window.onload = load;

Hugo

If you’re using Hugo then and running purgecss then you seem to need to add some empty spans with the light and dark classes to ensure the purging executes correctly in production mode:

<div>
    <button class="colour-mode__btn hidden dark:block focus:outline-none">
        <span>{{ partial "svg/sun.svg" . }}</span>
    </button>
    <button class="colour-mode__btn dark:hidden focus:outline-none">
        <span>{{ partial "svg/moon.svg" . }}</span>
    </button>
    <!-- ensure hugo captures these classes in hugo_stats.json when we run purgecss -->
    <span class="light"></span><span class="dark"></span>
</div>

Your <theme>/assets/css/postcss.config.js should look something like:

const themeDir = __dirname + "/../../";

const removeNewlines = (string) => string.replace(/\n$/g, "");

// See https://gohugo.io/hugo-pipes/postprocess/#css-purging-with-postcss
const purgecss = require("@fullhuman/postcss-purgecss")({
  content: [
    "./hugo_stats.json",
    "../../hugo_stats.json",
    "./exampleSite/hugo_stats.json",
  ],
  defaultExtractor: (content) => {
    let els = JSON.parse(content).htmlElements;
    return els.tags.concat(els.classes, els.ids).map(removeNewlines);
  },
});

// See https://tailwindcss.com/docs/configuration#using-a-different-file-name

module.exports = {
  plugins: [
    require("postcss-import")({ path: [themeDir] }),
    require("tailwindcss")({config: themeDir + 'assets/css/tailwind.config.js'}),
    require("autoprefixer")({ path: [themeDir] }),
    ...(process.env.HUGO_ENVIRONMENT === "production" || process.env.NODE_ENV === "production"? [purgecss] : []),
  ],
};

Our folder structure is:

themes/<theme>/assets/css
├── main.css
├── postcss.config.js
└── tailwind.config.js