
How to Add Dark Mode to Your Laravel Website (SSR) Using Tailwind 4.x
TLDR;
In this post we'll see how simple it is to add a dark mode toggle to your Laravel website using Tailwind 4.x. Although there are a few different ways to accomplish this, this post focuses on how it can be done in the context of a server-side rendered (SSR) site, without pulling in React or Vue. The example will use vanilla Javascript to handle the button click along with localStorage
for remembering the user's choice between visits. We'll also see how to use the dark:
class prefix to actually change the text and background colors of the site.
Step 1: Enable the Tailwind Dark Variant
The first step is to define a custom variant for dark mode directly in our main CSS file.
// resources/css/app.css
@custom-variant dark (&:where(.dark, .dark *));
This line tells Tailwind to look for the .dark
class on the root element (or any parent) and apply dark styles accordingly.
Step 2: Add the Toggle Icon to the Header
Next let's add the toggle button to the site header. On a personal note, I always like starting with the front-end when working on features like this. Gives a nice little boost of motivation to add the Javascript functionality once things are looking pretty on the front-end.
<!-- views/components/header.blade.php (or wherever your header is) -->
<button id="theme-toggle" type="button" aria-label="Toggle dark mode" class="border border-gray-200 dark:border-gray-300 cursor-pointer rounded-full p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 hidden dark:block">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 dark:hidden">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
</button>
Here we've given the button an id attribute of theme-toggle
. This will allow us to write some plain vanilla Javascript to handle the click event. Note that we've also added some dark:
class prefixes to show the appropriate icon depending on the current theme mode.
Step 3: Add the Button Toggle Logic Using Vanilla Javascript
Now that we have our little toggle button in the header, let's wire it up based on the id
attribute and use the browser's built-in localStorage
to remember the user's preference.
// resources/js/app.js
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle')
themeToggle.addEventListener('click', () => {
const theme = document.documentElement.classList.contains('dark') ? 'light' : 'dark'
localStorage.setItem('theme', theme)
window._setTheme(theme)
})
})
We also need to write some code to actually add or remove the dark
class from the root html element, depending on the mode we're currently in. So let's do that next.
Note: I actually went and peaked into how a few of my favourite sites were handling this, and apparently having this be an inline script near the opening head tag works best. I was initially attempting to do this after the DOM was fully loaded, and was running into some page flickering issues (since it was for a server-side rendered app). So let's make it a separate blade component and pull it into our main app layout.
<!-- resources/views/components/theme-toggle-script.blade.php -->
<script type="text/javascript">
if (!('_setTheme' in window)) {
window._setTheme = function setTheme(theme) {
if (theme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
try {
_setTheme(localStorage.theme)
} catch (_) {}
}
</script>
<!-- resources/views/layouts/app.blade.php -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Page') | {{ config('app.name') }}</title>
<link rel="icon" href="{{ asset('favicon.png') }}" type="image/png">
<-- Theme toggle scripts goes here, closer to the opening head tag (if possible) -->
<x-theme-toggle-script />
Step 4: Sprinkling in the dark: Class Prefixes Throughout Your Site
Now that the major nuts and bolts are in place, the real grunt work begins. Here you'll need to add the dark:
class prefixes throughout your codebase to fine-tune how the site should look in dark mode. Luckily this probably won't be as bad as it sounds. For simple sites, it will mean things like making the background darker and the text lighter. This is one area where I think AI auto-completions are definitely your friend.
<div class="text-center flex space-x-8 lg:space-x-12 justify-center py-12 px-4 dark:bg-black">
<a href="{{ route('about') }}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 text-sm uppercase">About Us</a>
<!-- ... -->
</div>
Wrap-Up
With Tailwind 4.x, Laravel Blade components, and a tiny bit of plain old Javascript, we can add a dark mode toggle without that much complexity. And as many studies now link blue light at night with sleep problems of various sorts, this feature can go a long way towards helping your users out. Especially if they are the late night coding types.
Happy coding!