Skip to content

Dark Mode in Laravel: Complete Implementation Guide with Tailwind CSS

1 min read
Dark Mode in Laravel: Complete Implementation Guide with Tailwind CSS

Dark mode has become an essential feature in modern web applications. Users expect the ability to switch between light and dark themes, and implementing this feature properly can significantly improve user experience and accessibility. In this guide, we'll explore how to implement dark mode in Laravel applications using Tailwind CSS, proper state management, and best practices.

Why Dark Mode Matters

Dark mode isn't just a trendy feature—it serves real purposes:

  • Reduced eye strain in low-light environments
  • Battery conservation on OLED displays
  • Accessibility improvements for users with light sensitivity
  • Modern user expectations across platforms

Setting Up Tailwind CSS for Dark Mode

First, ensure your Tailwind CSS configuration supports dark mode. In your tailwind.config.js:

module.exports = {
  darkMode: 'class', // Enable class-based dark mode
  content: [
    './resources/**/*.blade.php',
    './resources/**/*.js',
    './resources/**/*.vue',
  ],
  theme: {
    extend: {
      colors: {
        // Custom dark mode colors
        dark: {
          50: '#f8fafc',
          100: '#f1f5f9',
          900: '#0f172a',
          950: '#020617',
        }
      }
    },
  },
}

The darkMode: 'class' setting allows you to toggle dark mode by adding or removing a dark class from your HTML element.

Creating a Theme Toggle Component

Let's build a proper theme toggle using Livewire for state management:

<?php

namespace App\Livewire;

use Livewire\Component;

class ThemeToggle extends Component
{
    public $darkMode = false;

    public function mount()
    {
        // Check user preference from session or default to system preference
        $this->darkMode = session('dark-mode', false);
    }

    public function toggleTheme()
    {
        $this->darkMode = !$this->darkMode;
        session(['dark-mode' => $this->darkMode]);
        
        $this->dispatch('theme-changed', darkMode: $this->darkMode);
    }

    public function render()
    {
        return view('livewire.theme-toggle');
    }
}

And the corresponding Blade view using Aura UI components:

<div class="flex items-center space-x-3">
    <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
        {{ $darkMode ? 'Dark' : 'Light' }} Mode
    </span>
    
    <x-aura::button
        variant="ghost"
        size="sm"
        wire:click="toggleTheme"
        class="p-2"
    >
        @if($darkMode)
            <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
            </svg>
        @else
            <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
            </svg>
        @endif
    </x-aura::button>
</div>

<script>
    document.addEventListener('livewire:initialized', () => {
        // Apply theme on component initialization
        const darkMode = @js($darkMode);
        document.documentElement.classList.toggle('dark', darkMode);
        
        // Listen for theme changes
        Livewire.on('theme-changed', (event) => {
            document.documentElement.classList.toggle('dark', event.darkMode);
        });
    });
</script>

Advanced Theme Management with Alpine.js

For a more sophisticated approach that respects system preferences and provides smooth transitions:

// resources/js/theme.js
function initTheme() {
    return {
        darkMode: false,
        
        init() {
            // Check for saved theme preference or default to system preference
            this.darkMode = localStorage.getItem('theme') === 'dark' || 
                (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
            
            this.updateTheme();
            
            // Listen for system theme changes
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
                if (!localStorage.getItem('theme')) {
                    this.darkMode = e.matches;
                    this.updateTheme();
                }
            });
        },
        
        toggleTheme() {
            this.darkMode = !this.darkMode;
            localStorage.setItem('theme', this.darkMode ? 'dark' : 'light');
            this.updateTheme();
        },
        
        updateTheme() {
            document.documentElement.classList.toggle('dark', this.darkMode);
            
            // Update meta theme-color for mobile browsers
            const metaThemeColor = document.querySelector('meta[name="theme-color"]');
            if (metaThemeColor) {
                metaThemeColor.setAttribute('content', this.darkMode ? '#1f2937' : '#ffffff');
            }
        }
    }
}

// Make it globally available
window.initTheme = initTheme;

Implementing Dark Mode in Your Layout

Update your main layout file to support dark mode:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" x-data="initTheme()" x-init="init()">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#ffffff">
    
    <title>{{ $title ?? 'Laravel App' }}</title>
    
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
    <nav class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex items-center">
                    <h1 class="text-xl font-semibold">My Laravel App</h1>
                </div>
                
                <div class="flex items-center space-x-4">
                    <!-- Theme Toggle -->
                    <x-aura::button
                        variant="ghost"
                        size="sm"
                        @click="toggleTheme()"
                        class="p-2"
                    >
                        <svg x-show="!darkMode" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                            <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
                        </svg>
                        <svg x-show="darkMode" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                            <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
                        </svg>
                    </x-aura::button>
                </div>
            </div>
        </div>
    </nav>
    
    <main class="min-h-screen">
        {{ $slot }}
    </main>
</body>
</html>

Dark Mode Best Practices

1. Use Semantic Color Classes

Instead of hardcoding colors, use semantic classes that work in both themes:

<x-aura::card class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
    <x-aura::card.header>
        <h3 class="text-gray-900 dark:text-gray-100">Card Title</h3>
    </x-aura::card.header>
    
    <x-aura::card.body>
        <p class="text-gray-600 dark:text-gray-300">
            Content that adapts to both light and dark themes.
        </p>
    </x-aura::card.body>
</x-aura::card>

2. Handle Images and Media

Some images may need different versions for dark mode:

<picture>
    <source srcset="{{ asset('images/logo-dark.png') }}" media="(prefers-color-scheme: dark)">
    <img src="{{ asset('images/logo-light.png') }}" alt="Logo" class="h-8 w-auto">
</picture>

3. Test Accessibility

Ensure proper contrast ratios in both themes:

/* Custom CSS for enhanced dark mode support */
.dark {
    --tw-prose-body: #d1d5db;
    --tw-prose-headings: #f9fafb;
    --tw-prose-links: #60a5fa;
    --tw-prose-bold: #f9fafb;
    --tw-prose-counters: #9ca3af;
    --tw-prose-bullets: #6b7280;
}

Persisting User Preferences

For authenticated users, consider storing theme preferences in the database:

// Migration
Schema::table('users', function (Blueprint $table) {
    $table->string('theme_preference')->default('system');
});

// User model
class User extends Authenticatable
{
    protected $fillable = [
        'name', 'email', 'password', 'theme_preference'
    ];
    
    public function getPrefersDarkModeAttribute()
    {
        return $this->theme_preference === 'dark';
    }
}

Conclusion

Implementing dark mode properly requires attention to user experience, accessibility, and technical implementation details. By using Tailwind CSS's built-in dark mode utilities, proper state management with Livewire or Alpine.js, and following accessibility best practices, you can create a seamless theme switching experience.

The key is to think beyond just inverting colors—consider the entire user experience, from respecting system preferences to maintaining proper contrast ratios. With components like those provided by Aura UI, you get dark mode support out of the box, allowing you to focus on the unique aspects of your application while ensuring a consistent, professional appearance across both themes.

Remember to test your dark mode implementation thoroughly across different devices and browsers, and always prioritize accessibility to ensure all users can enjoy your application regardless of their preferred theme.

Enjoyed this? Get the next one

Practical Laravel UI tips and new components, straight to your inbox. Free, no spam.