Tutorials / / 4 min read
How to create a dark mode toggle with Tailwind CSS: JavaScript and Alpine.js
Build a dark mode toggle with Tailwind's dark variant, two ways: a few lines of vanilla JavaScript or an Alpine.js x-data binding.
Today is toggle day! We are building a dark mode toggle, twice: first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a dark mode toggle?
A dark mode toggle lets users flip your site between light and dark color schemes. It’s a small feature with real benefits: dark interfaces are easier on the eyes in low light, can save battery on OLED screens, and give your site a modern feel while respecting each user’s preference.
Use cases
- User preference: many people simply prefer dark interfaces, especially at night.
- Reducing eye strain: lowering the overall screen brightness helps during long sessions.
- Energy efficiency: OLED and AMOLED screens consume less power when displaying dark colors.
- Accessibility: offering both modes reduces glare for users with light sensitivity or visual impairments.
The markup
Tailwind makes this easy: it provides dark mode utility classes that you append to the elements whose colors should change.
dark:bg-gray-900on the wrapper sets the background color when dark mode is enabled.dark:text-whiteon the heading sets the text color when dark mode is enabled.
<div class="bg-white dark:bg-gray-900">
<!-- Your content here -->
</div><h1 class="text-black dark:text-white">
<!-- Your text here -->
</h1>All you have to do is append the dark mode utility class to the element you want to change the color of. If you do not want to have a toggle, you can just add the class to the element directly and it will be applied when the user preferences are set to dark mode on their device, browser, or OS.
The script
document.addEventListener("DOMContentLoaded", function () {: runs when the DOM is fully loaded.const toggleButton = document.getElementById("dark-mode-toggle");: selects the element with the ID “dark-mode-toggle”.const body = document.body;: selects the body element.body.classList.toggle("dark");: toggles thedarkclass on every click, which switches all the dark variants on and off.
document.addEventListener("DOMContentLoaded", function () {
const toggleButton = document.getElementById("dark-mode-toggle");
const body = document.body;
toggleButton.addEventListener("click", function () {
body.classList.toggle("dark");
});
});The Alpine.js version
Same component, no separate script: the state lives in x-data.
x-data="{ darkMode: false }": stores the current state of the theme.@click="darkMode = !darkMode": toggles it when the button is clicked.x-show="!darkMode"andx-show="darkMode": swap the button label and icons based on the value of thedarkModevariable.
<div>
<button
@click="darkMode = !darkMode">
<span
x-show="!darkMode">
Toggle Dark Mode
<!-- Here goes the icon for the dark mode -->
</span>
<span
x-show="darkMode">
Toggle Light Mode
<!-- Here goes the icon for the light mode -->
</span>
</button>
</div>For the classes to react to a toggle, we need to tell Tailwind that dark mode is class-based. Normally Tailwind’s dark mode follows the prefers-color-scheme media query. In Tailwind CSS v4, override the dark variant in your CSS:
@custom-variant dark (&:where(.dark, .dark *));If you are still on Tailwind CSS v3, the same thing goes in tailwind.config.js:
module.exports = {
darkMode: "class",
// Other config options
};After that, add x-data="{ darkMode: false }" to the body tag. This tells Alpine to initialize the darkMode variable to false when the page loads, and it scopes where the toggle applies: add it to just a component and only that component will be affected.
<body x-data="{ darkMode: false }">
<!--- Content -->
</body>This is how your component will look, for example, if you want the body to be dark when the dark theme is toggled:
<body x-data="{ darkMode: false }" class="bg-white dark:bg-gray-900">
<!--- Content -->
</body>You can try the Alpine version separately: live demo and source code.
Which one should you use?
Both versions boil down to toggling a single class. If Alpine is already loaded, x-data keeps the state and the label swap right in the markup; otherwise the vanilla listener does the job in a few lines with no dependency.
Conclusion
A dark mode toggle is one class and a click handler, and Tailwind’s dark variants handle the rest. Customize it to your liking and add more features like a color picker or a full theme switcher.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza