Depiction of dark mode as cartoon kid in mask at night time

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.

CSS
// 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.

BLADE
<!-- 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.

JS
// 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.

BLADE
<!-- 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>
BLADE
<!-- 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 />
Pro Tip Some sites go even further to automatically set the theme based on the user's device preferences. This is done using the prefers-color-scheme media query. Although it's not the main focus of this post, there are a lot of additional resources online showing how to add this in with some fairly minimal code tweaks.

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.

HTML
<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!

Up Next

12 Simple Tips for Securing Your Web Apps . ' Featured Image'

12 Simple Tips for Securing Your Web Apps

In this post I share some simple tips you can use to improve the security of your web applications. Although many of these are best practices which apply to cloud-hosted web apps in general, I'll also share a few Laravel-specific code samples to...

August 1, 2025 • 7 min read
Adding IP Whitelisting to Your Filament PHP Admin Panel . ' Featured Image'

Adding IP Whitelisting to Your Filament PHP Admin Panel

Want to restrict access to your Filament admin panel to only specific IPs? Here's the coles notes.

July 30, 2025 • 5 min read