Adding Dark Mode
Fluxtor includes a comprehensive dark mode system that automatically detects user preferences, prevents flickering. The implementation uses Alpine.js for state management and Tailwind CSS for styling.
Using CLI
when using our CLI it do most of work for you, when running
php artisan fluxtor:init
it will add :
resources/js/utils.js
a utility for registering reactive magic propertyresources/js/globals/theme.js
here's full implementation of adding dark mode to your app
Implementation Overview
This is the cleaner way we discover so far to add dark mode to your apps
The dark mode system integrates seamlessly with Fluxtor's theme-switcher
component, which handles all user interactions and theme management automatically. The component fires theme-changed
events that the theme switcher script listens to, providing a complete theming solution.
Step 1: Setup the Utility
using the same way we manage modals
, toasts
, other global things you may consider
// resources/js/utils.js export default function defineReactiveMagicProperty(name, rawObject) { const instance = Alpine.reactive(rawObject); /** reactive objects are plain proxies and does not support hooks like stores, or scopes in alpine so we init manualy */ if (typeof instance.init === 'function') { instance.init(); } Alpine.magic(name, () => instance); // ex : if the magic called $modal we register Modal into the window window[name[0].toUpperCase() + name.slice(1)] = instance; }
this is responsible for registering reactive object and expose it to be used as magic alpinejs property, and also bind it globally with Window
Object
Step 2: Register $theme magic property
// resources/js/globals/theme.js import defineReactiveMagicProperty from '../utils'; document.addEventListener('alpine:init', () => { defineReactiveMagicProperty('theme', { currentTheme: null, storedTheme: null, init() { // at first we check local storage this.storedTheme = localStorage.getItem('theme') ?? 'system'; // resolve the configured theme to be set only [light, dark] this.currentTheme = computeTheme(this.storedTheme); // Apply initial theme to DOM applyTheme(this.currentTheme); // Listen for system theme changes let media = window.matchMedia('(prefers-color-scheme: dark)'); media.addEventListener('change', (event) => { if (this.storedTheme === 'system') { this.currentTheme = event.matches ? 'dark' : 'light'; applyTheme(this.currentTheme); } }); }, setTheme(newTheme) { this.storedTheme = newTheme; localStorage.setItem('theme', newTheme); this.currentTheme = computeTheme(newTheme); applyTheme(this.currentTheme); }, setLight() { this.setTheme('light'); }, setDark() { this.setTheme('dark'); }, setSystem() { this.setTheme('system'); }, toggle() { if (this.storedTheme === 'system') { // If system, toggle to opposite of current computed theme this.setTheme(this.currentTheme === 'dark' ? 'light' : 'dark'); } else { // Toggle between light and dark this.setTheme(this.storedTheme === 'dark' ? 'light' : 'dark'); } }, get() { return { stored: this.storedTheme, current: this.currentTheme, isLight: this.isLight, isDark: this.isDark, isSystem: this.isSystem }; }, // Getter methods for easy template usage get isLight() { return this.storedTheme === 'light'; }, get isDark() { return this.storedTheme === 'dark'; }, get isSystem() { return this.storedTheme === 'system'; }, // some times we need to show only light or dark not system mode, so we handle // these senarions using this getter that take care when the old mode is system get isResolvedToLight() { if (this.isSystem) { return resolved() === 'light' } else { return this.isLight; } }, get isResolvedToDark() { if (this.isSystem) { return resolved() === 'dark' } else { return this.isDark; } } }) }); // static function function computeTheme(themePreference) { if (themePreference === 'system') { return resolved(); } return themePreference; } function resolved() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } function applyTheme(theme) { if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }
Step 3: import the file
// resource/js/app.js import './globals/theme.js';
Step 4: Prevent Flickering
Add flicker prevention scripts directly in your HTML head to ensure themes load before content renders:
<!-- resources/views/layouts/app.blade.php --> <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>Your App {{ isset($title) ? '| ' . $title : '' }}</title> @vite(['resources/css/app.css']) <script> // Load dark mode before page renders to prevent flicker const loadDarkMode = () => { const theme = localStorage.getItem('theme') ?? 'system' if ( theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)') .matches) ) { document.documentElement.classList.add('dark') } } // Initialize on page load loadDarkMode(); // Reinitialize after Livewire navigation (for spa mode) document.addEventListener('livewire:navigated', function() { loadDarkMode(); }); </script> </head> <body class="bg-gray-100 dark:bg-black text-gray-800 dark:text-white"> {{ $slot }} @livewireScriptConfig @vite(['resources/js/app.js']) <!-- Ensure dark mode is applied after scripts load, this is also required to prevent flickering when many livewire component changes indepently --> <script> loadDarkMode() </script> </body> </html>
Step 5: Add Theme Switcher Component
Fluxtor includes a comprehensive theme-switcher component with multiple variants. Add it to your layout or navigation:
{{-- Basic dropdown theme switcher --}} <x-ui.theme-switcher variant="dropdown" /> {{-- Stacked toggle variant --}} <x-ui.theme-switcher variant="stacked" /> {{-- Inline buttons variant --}} <x-ui.theme-switcher variant="inline" />
go the official docs of the theme switcher component
Testing Themes
Test your components in all three theme modes:
- Light mode
- Dark mode
- System preference (both light and dark)
Troubleshooting
Theme Not Persisting
Ensure localStorage is available and not blocked by privacy settings.
Flickering on Load
Make sure the flicker prevention script runs before any content renders and also leverage using livewire nivation events.
System Theme Not Detected
Verify that the prefers-color-scheme
media query is supported in your target browsers.