Tailwind css dark mode switch with JavaScript
Tags: design, tailwindcss, javascript, tutorial
Published: 2020-08-25 21:34:46

Tailwind css is really a greate utility-first framework that provides a lot of preset values (colors, sizes, etc...) that work very well out of the box. I also like the fact that I dont have to jump between the html and the css file, while others prefer the separation.

Now the problem is that tailwind makes it harder to implement a dark or a colored version, unless you know how it is done. Without tailwind I would add a class like 'scheme-dark' to the html tag and customize every element in my scss file like this:
```
/* style.scss */

h1 {
    color: black;
}

.scheme-dark {
    h1 {
        color: white;
    }
}
```
However in tailwind we define the color of the text with a class in the html file, so this is what we want:
```
/* index.html */

<h1 class="text-blue-900 dark:text-white">Hello world!</h1>
```
The official documentation recommends to add the following to the tailwind config
```
/* tailwind.config.js */

module.exports = {
  theme: {
    extend: {
      screens: {
        'dark': {'raw': '(prefers-color-scheme: dark)'},
        // => @media (prefers-color-scheme: dark) { ... }
      }
    }
  }
}
```
This works, but not as good as expected. Why? Because the media rule `prefers-color-scheme` looks at the browser setting, it is not possible to change it with e.g. a button and some javascript. So the user would have to go into the browser settings and change to light/dark mode.

To give the user the option to change to light/dark or any other color mode, we can modify the tailwind configs.

First we create our custom variant by adding a new plugin in the tailwind configs:
```
    ...
    plugins: [
        plugin(function ({ addVariant, prefix }) {
            addVariant('dark', ({ modifySelectors, separator}) => {
                modifySelectors(({ selector }) => {
                    return selectorParser((selectors) => {
                        selectors.walkClasses((sel) => {
                            sel.value = `dark${separator}${sel.value}`
                            sel.parent.insertBefore(sel, selectorParser().astSync('.scheme-dark '))
                        })
                    }).processSync(selector)
                })
            })
        })
    ]
    ...
```
The name of our variant is `dark` and it has a parent class `.scheme-dark ` (don't forget the space at the end!)? This will be used by tailwind when it generates the css.

Then we add our custom variant to the properties that we want to use:
```
    ...
    variants: {
        textColor: ['dark', 'responsive', 'hover', 'focus'],
        backgroundColor: ['dark', 'responsive', 'hover', 'focus']
    },
    ...
```
Tailwind will now generate every text color class and background color class additionally with the `.dark:\` prefix with the parent class `.scheme-dark`. So e.g. for the text color `text-white` it will create the following css:
```
.text-white {
    color: #fff;
}

.scheme-dark .dark:\text-white {
    color: #fff;
}
```
So we can now simply add the `scheme-dark` to our html tag and define a text/background color like `<h1 class="text-black dark:text-white" >Hello</h1>` when dark mode is enabled.

```
<script>
    const html = document.getElementsByTagName('html')[0];    

    function toggleDarkMode() {
        if(html.classList.contains('scheme-dark')) {
            html.classList.remove('scheme-dark');
        } else {
            html.classList.add('scheme-dark');
        }
    }
</script>

<button onclick="toggleDarkMode()">Toggle dark mode</button>
```


Here is the complete tailwind config file:
```
const plugin = require("tailwindcss/plugin");
const selectorParser = require("postcss-selector-parser");

module.exports = {
    theme: {
    ...
    },
    variants: {
        textColor: ['dark', 'responsive', 'hover', 'focus'],
        backgroundColor: ['dark', 'responsive', 'hover', 'focus']
    },
    plugins: [
        plugin(function ({ addVariant, prefix }) {
            addVariant('dark', ({ modifySelectors, separator}) => {
                modifySelectors(({ selector }) => {
                    return selectorParser((selectors) => {
                        selectors.walkClasses((sel) => {
                            sel.value = `dark${separator}${sel.value}`
                            sel.parent.insertBefore(sel, selectorParser().astSync(prefix('.scheme-dark ')))
                        })
                    }).processSync(selector)
                })
            })
        })
    ]
}
```

Now you might ask me: **What If I want to change the color when hovering the text in dark mode?**

No problemo amigo! Justo addo uno plugino:
```
        plugin(function ({ addVariant, e }) {
            addVariant('dark-hover', ({ modifySelectors, separator}) => {
                modifySelectors(({ className }) => {
                    return `.scheme-dark .${e(`dark\:hover${separator}${className}`)}:hover`
                })
            })
        })
```
and add the variant:
```
    variants: {
        textColor: ['responsive', 'dark', 'dark-hover', 'hover', 'focus'],
    },
```
Now we can do this:
```
<h1 class="text-black dark:text-white dark:hover:text-red-600 hover:text-blue-600">Hover me</h1>
```
Remember, this is only the dark mode, you could also do the same for colored versions!

If you use postcss to remove unused css (recommended!) like
```
module.exports = {
    purge: [
        './build/app/views/**/*.php',
        './build/public/**/*.php',
    ],
    ...
}
```
then you need to add an empty div with the class `scheme-dark`:
````
<div class="scheme-dark"></div>
```
If you don't do this every `scheme-dark` class will be removed!