Let's be honest—what kind of developer are you if you haven't added dark mode to your personal projects? It's practically mandatory at this point. If your site isn't ready to embrace the darkness, don't worry. Today, we're fixing that.
Dark mode isn't just cool, it's a necessity. It saves your eyes and looks slick. In this guide, we'll add a dark mode toggle to your Astro site using Tailwind CSS, making sure it respects system preferences and remembers user choices.
First things first—let's make sure Tailwind is set up to handle dark mode properly. If you're using Tailwind V4, you will probably want to add this custom variant to your Tailwind CSS configuration if you're not using a tailwind.config.js
file:
@custom-variant dark (&:where(.dark, .dark *));
If you are still using an older version of Tailwind prior to V4, you can add the following to your tailwind.config.js
file:
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'selector',
// ...
}
This ensures that dark mode styles apply when the .dark
class is present on the <html>
element.
No one wants to get flashbanged by a white screen when they clearly chose dark mode. To fix that, add this script inside your <head>
:
<script is:inline>
// Ensures dark mode is applied before styles load
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
This script runs immediately to prevent a flash of unstyled light mode. The is:inline
attribute ensures that Astro inlines the script into the HTML, allowing it to execute as early as possible.
If you're using Astro's page transitions (now ClientRouter
), you'll also want to reapply dark mode after navigation swaps:
<script is:inline>
document.addEventListener('astro:after-swap', () => {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
}
});
</script>
If you're not using page transitions, you can skip this part.
Here's a simple React component to handle the actual toggle component:
import { useEffect, useState } from 'react';
const DarkModeToggle = () => {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
setDarkMode(document.documentElement.classList.contains('dark'));
}, []);
const toggleDarkMode = () => {
document.body.style.transition = 'color 0.1s, background-color 0.3s';
if (darkMode) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
setDarkMode(!darkMode);
};
return (
<button onClick={toggleDarkMode} aria-label="Toggle dark mode" className="p-2">
{darkMode ? '🌙' : '☀️'}
</button>
);
};
export default DarkModeToggle;
Minimal, effective, and most importantly—dark mode enabled.
You could just dump this anywhere, but let's be civil and place it neatly in the navbar:
---
import DarkModeToggle from '../components/DarkModeToggle';
---
<nav class="p-4 flex justify-between">
<h1>My Website</h1>
<DarkModeToggle client:only="react" />
</nav>
Since DarkModeToggle
is a React component, we use client:only="react"
to tell Astro to hydrate it only on the client while keeping the rest of the navbar static - you can read more about that here . Now your users can seamlessly switch between dark and light mode.
Lastly, you just need to add the necessary utility classes to your components. For example on our body, we will add bg-white dark:bg-black
. When the dark
class is present on the <head>
, the bg-black
code will be applied to our body. For a better explanation, see the Tailwind docs .
Congratulations! 🎉 Your Astro website is now part of the dark side (or light side, if that's your thing). With Tailwind's .dark
class, everything stays simple and controlled, while our React toggle makes switching a breeze.
Your eyes will thank you. 🚀