- Overview
- Setup
- Toggle Button
- Tailwind dark: Variant
- Automatic Dark Mode Support
- Using with Livewire
- Configuration
- Overriding Dark Colors
- Next Steps
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:
@custom-variant dark-- Aura UI'saura.cssincludes@custom-variant dark (&:is(.dark *));which overrides Tailwind 4's default dark mode to use the.darkclass on<html>instead ofprefers-color-scheme- Alpine.js -- Manages the toggle state and persists the user's preference
- CSS custom properties -- The
base/dark-mode.cssfile 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
localStorageon page load - Falls back to the system
prefers-color-schemeif no manual preference exists - Listens for system theme changes in real time
- Provides
toggle(),setLight(),setDark(), andsetSystem()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.darkclass 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
- Theming -- Customize colors, fonts, and design tokens
- Configuration -- Configure dark mode strategy and other options
- Installation -- Full setup instructions