Adding a dark mode to your website is a great way to improve accessibility. In this guide, we will learn how to implement a perfect dark mode in your Astro project using Tailwind CSS. You can use any framework you prefer, but we will be using Preact for the UI creation.
To begin, create a new Astro project:
Next, install the TailwindCSS and Preact integrations:
Add both integrations to your
Create a minimal TailwindCSS config file in the root of your project. Make sure to modify the
content property to include all the files that contain your styles. Also, set the
darkMode property to
"class" to enable dark mode:
Astro provides a feature to add inline scripts directly to your Astro files, which run as soon as the HTML is loaded. This prevents the “flash of inaccurate color theme” issue that commonly occurs when implementing dark mode with hydration. You can find more information about inline scripts in the Astro documentation.
The following code retrieves the user’s preferred theme and applies it to the HTML element. You can copy/paste or modify this code snippet in your Astro project. We will explain each line of code in the next paragraph.
theme variable is an immediately invoked function expression (IIFE) that returns the current theme based on the user’s preference. The first
if statement checks if the user has a previously saved theme in localStorage. If so, it returns that theme. The second
if statement checks if the user prefers dark mode based on their system settings. If so, it returns
"dark". If none of the conditions are met, it returns
"light". Once the theme is defined, we use it to add or remove the
"dark" class from the HTML element and save the theme to localStorage.
Creating the UI
In Astro, you can use any UI framework of your choice. For this example, we will use Preact due to its small size and performance. The following code snippet renders a button that toggles between dark and light mode:
Rendering Components on the Server
Regardless of the UI framework you use, if you are using Static Site Generation (SSG), Astro will render your UI components on the server at build time and hydrate them on the client side. This feature improves website performance, accessibility, and SEO.
However, this feature also has some trade-offs. Since components are rendered on the server, web APIs like
window are not available.
Fallback Initial State
To overcome this limitation, you can add a fallback initial state that will be used during build time and then updated to the correct state after hydration. For example:
In the above code, we attempt to get the theme from
localStorage, and if it’s not available, we use
"light" as the initial state. Using a fallback initial state is a common approach to solve this problem. However, it can lead to a “client/server state mismatch” issue, where the initial state differs from the state after hydration.
Addressing the Client/Server Mismatch
One way to address the client/server mismatch is by adding a
mounted state. This state ensures that your component’s rendering waits until it is mounted to the DOM, making all the web APIs available and ensuring that the initial state matches the state after hydration. You can achieve this using the
useEffect hooks to create a mounted state. Here’s an example:
By checking the
isMounted state, we can render a fallback UI or
null until the component is mounted. Once it’s mounted, the actual UI will be rendered.