Skip to content

Overview

Every Aura UI component ships with full dark mode support. When dark mode is active, components automatically switch to dark-optimized colors, shadows, and contrast levels. No additional configuration per component is required.

Dark mode in Aura UI relies on three things:

  1. @custom-variant dark -- Aura UI's aura.css includes @custom-variant dark (&:is(.dark *)); which overrides Tailwind 4's default dark mode to use the .dark class on <html> instead of prefers-color-scheme
  2. Alpine.js -- Manages the toggle state and persists the user's preference
  3. CSS custom properties -- The base/dark-mode.css file overrides surface, accent, shadow, and glow tokens under .dark

Setup

Step 1: Add Alpine.js Data to the HTML Tag

In your root layout file (e.g., resources/views/layouts/app.blade.php), add the darkMode Alpine.js data component to the <html> tag:

<!DOCTYPE html>
<html
    lang="{{ str_replace('_', '-', app()->getLocale()) }}"
    x-data="darkMode"
    x-bind:class="{ 'dark': dark }"
>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ config('app.name') }}</title>

    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|jetbrains-mono:400,500" rel="stylesheet" />

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 antialiased">
    {{ $slot }}
</body>
</html>

The key part is x-data="darkMode" and x-bind:class="{ 'dark': dark }". When dark is true, the .dark class is added to <html>, and all Aura UI components switch to their dark variants.

Step 2: Register the Alpine.js Component

Create the darkMode Alpine data component in your JavaScript:

// resources/js/app.js
import Alpine from 'alpinejs';

document.addEventListener('alpine:init', () => {
    Alpine.data('darkMode', () => ({
        dark: false,

        init() {
            // Check localStorage first, then system preference
            const stored = localStorage.getItem('darkMode');

            if (stored !== null) {
                this.dark = stored === 'true';
            } else {
                this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            }

            // Watch for system preference changes (when no manual override)
            window.matchMedia('(prefers-color-scheme: dark)')
                .addEventListener('change', (e) => {
                    if (localStorage.getItem('darkMode') === null) {
                        this.dark = e.matches;
                    }
                });
        },

        toggle() {
            this.dark = !this.dark;
            localStorage.setItem('darkMode', this.dark);
        },

        setLight() {
            this.dark = false;
            localStorage.setItem('darkMode', 'false');
        },

        setDark() {
            this.dark = true;
            localStorage.setItem('darkMode', 'true');
        },

        setSystem() {
            localStorage.removeItem('darkMode');
            this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
        },
    }));
});

window.Alpine = Alpine;
Alpine.start();

This component:

  • Reads the saved preference from localStorage on page load
  • Falls back to the system prefers-color-scheme if no manual preference exists
  • Listens for system theme changes in real time
  • Provides toggle(), setLight(), setDark(), and setSystem() methods

Step 3: Prevent Flash of Incorrect Theme

To avoid a flash of the wrong theme on page load (FOIT), add an inline script in the <head> before any CSS or JavaScript loads:

<head>
    {{-- Prevent dark mode flash --}}
    <script>
        (function() {
            const stored = localStorage.getItem('darkMode');
            const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            if (stored === 'true' || (stored === null && prefersDark)) {
                document.documentElement.classList.add('dark');
            }
        })();
    </script>

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

This synchronous script runs before the page renders, adding the .dark class immediately if needed.

Toggle Button

Simple Toggle

A minimal dark mode toggle button with sun and moon icons:

<button
    x-on:click="toggle"
    type="button"
    class="relative inline-flex items-center justify-center w-10 h-10 rounded-lg
           text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100
           hover:bg-gray-100 dark:hover:bg-gray-800
           transition-colors duration-150"
    :aria-label="dark ? 'Switch to light mode' : 'Switch to dark mode'"
>
    {{-- Sun icon (visible in dark mode) --}}
    <svg
        x-show="dark"
        x-transition:enter="transition ease-out duration-200"
        x-transition:enter-start="opacity-0 rotate-[-90deg] scale-0"
        x-transition:enter-end="opacity-100 rotate-0 scale-100"
        class="w-5 h-5"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="2"
        stroke="currentColor"
    >
        <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
    </svg>

    {{-- Moon icon (visible in light mode) --}}
    <svg
        x-show="!dark"
        x-transition:enter="transition ease-out duration-200"
        x-transition:enter-start="opacity-0 rotate-90 scale-0"
        x-transition:enter-end="opacity-100 rotate-0 scale-100"
        class="w-5 h-5"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="2"
        stroke="currentColor"
    >
        <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 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 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
    </svg>
</button>

Three-Way Toggle (Light / Dark / System)

A more complete toggle that also allows users to defer to their system preference:

<div
    x-data="{ open: false }"
    class="relative"
>
    <button
        x-on:click="open = !open"
        type="button"
        class="inline-flex items-center justify-center w-10 h-10 rounded-lg
               text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100
               hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
        aria-label="Theme settings"
    >
        <svg x-show="dark" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
        </svg>
        <svg x-show="!dark" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 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 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
        </svg>
    </button>

    <div
        x-show="open"
        x-on:click.outside="open = false"
        x-transition
        class="absolute right-0 mt-2 w-36 rounded-lg bg-white dark:bg-gray-800
               border border-gray-200 dark:border-gray-700 shadow-lg py-1 z-50"
    >
        <button
            x-on:click="setLight(); open = false"
            class="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300
                   hover:bg-gray-100 dark:hover:bg-gray-700"
        >
            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
            </svg>
            Light
        </button>
        <button
            x-on:click="setDark(); open = false"
            class="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300
                   hover:bg-gray-100 dark:hover:bg-gray-700"
        >
            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 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 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
            </svg>
            Dark
        </button>
        <button
            x-on:click="setSystem(); open = false"
            class="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-300
                   hover:bg-gray-100 dark:hover:bg-gray-700"
        >
            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
            </svg>
            System
        </button>
    </div>
</div>

Using Aura UI Dropdown

You can also use the built-in dropdown component for a cleaner implementation:

<x-aura::dropdown>
    <x-slot name="trigger">
        <button
            type="button"
            class="inline-flex items-center justify-center w-10 h-10 rounded-lg
                   text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100
                   hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
        >
            <x-aura::icon x-show="dark" name="sun" class="w-5 h-5" />
            <x-aura::icon x-show="!dark" name="moon" class="w-5 h-5" />
        </button>
    </x-slot>

    <x-aura::dropdown.item x-on:click="setLight()">
        <x-aura::icon name="sun" class="w-4 h-4" />
        Light
    </x-aura::dropdown.item>

    <x-aura::dropdown.item x-on:click="setDark()">
        <x-aura::icon name="moon" class="w-4 h-4" />
        Dark
    </x-aura::dropdown.item>

    <x-aura::dropdown.separator />

    <x-aura::dropdown.item x-on:click="setSystem()">
        <x-aura::icon name="computer-desktop" class="w-4 h-4" />
        System
    </x-aura::dropdown.item>
</x-aura::dropdown>

Tailwind dark: Variant

Aura UI components use Tailwind's dark: variant internally. When you build your own views, use the same pattern:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    <h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200">
        Dashboard
    </h1>

    <p class="text-gray-600 dark:text-gray-400">
        Welcome back.
    </p>
</div>

Common Dark Mode Patterns

{{-- Surfaces --}}
<div class="bg-white dark:bg-gray-900">...</div>
<div class="bg-gray-50 dark:bg-gray-800">...</div>

{{-- Text --}}
<p class="text-gray-900 dark:text-gray-100">Primary text</p>
<p class="text-gray-500 dark:text-gray-400">Muted text</p>

{{-- Borders --}}
<div class="border border-gray-200 dark:border-gray-700">...</div>

{{-- Hover states --}}
<button class="hover:bg-gray-100 dark:hover:bg-gray-800">...</button>

{{-- Inputs --}}
<input class="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600
              text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500" />

Automatic Dark Mode Support

All Aura UI components handle dark mode internally. You do not need to add dark: classes to Aura UI components -- they adapt automatically:

{{-- These components look correct in both light and dark mode --}}
<x-aura::card>
    <x-aura::card.title>User Profile</x-aura::card.title>
    <x-aura::card.description>Manage your account settings.</x-aura::card.description>

    <x-aura::input label="Name" value="John Doe" />
    <x-aura::input label="Email" type="email" value="[email protected]" />

    <x-aura::form.actions>
        <x-aura::button variant="primary">Save Changes</x-aura::button>
        <x-aura::button variant="ghost">Cancel</x-aura::button>
    </x-aura::form.actions>
</x-aura::card>

<x-aura::alert variant="info">
    Your profile was updated successfully.
</x-aura::alert>

<x-aura::stats-card
    label="Total Revenue"
    value="$12,345"
    trend="+12.5%"
/>

All of the above render correctly in both themes without any extra classes.

Using with Livewire

If you use Livewire, the dark mode state persists across Livewire navigations because it is stored in localStorage and applied via the inline <head> script. No special Livewire configuration is needed.

However, if you use Livewire's wire:navigate for SPA-like navigation, make sure the flash-prevention script is in the persistent layout:

{{-- resources/views/components/layouts/app.blade.php --}}
<!DOCTYPE html>
<html
    lang="{{ str_replace('_', '-', app()->getLocale()) }}"
    x-data="darkMode"
    x-bind:class="{ 'dark': dark }"
>
<head>
    <script>
        (function() {
            const stored = localStorage.getItem('darkMode');
            const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            if (stored === 'true' || (stored === null && prefersDark)) {
                document.documentElement.classList.add('dark');
            }
        })();
    </script>

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 antialiased">
    {{ $slot }}
</body>
</html>

Configuration

The dark mode detection strategy is configured in config/aura-ui.php:

'dark_mode' => 'class',
Value Behavior
class Dark mode when .dark class is on <html> (default, user-toggled)

Note: Aura UI's CSS uses @custom-variant dark (&:is(.dark *)); which is always class-based. The dark mode is controlled by toggling the .dark class on the <html> element as described in the setup above.

Overriding Dark Colors

Aura UI's dark mode overrides are defined in base/dark-mode.css. The surface scale is inverted (light values become dark), accent colors glow brighter, shadows become deeper, and glow intensity increases.

To customize how components look in dark mode, override the CSS custom properties under the .dark selector:

/* resources/css/app.css — after importing aura.css */
.dark {
    /* Override surfaces */
    --color-aura-surface-0:   #0c1222;
    --color-aura-surface-50:  #162032;
    --color-aura-surface-100: #1e3050;

    /* Override accent brightness */
    --color-aura-primary-500: #a5b4fc;

    /* Override glow intensity */
    --aura-glow-intensity: 0.35;

    /* Override shadow depth */
    --shadow-aura-md: 0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
}

See the Theming guide for the full list of design tokens and the CSS architecture breakdown.

Next Steps