jeremykenedy/laravel-darkmode-toggle
jeremykenedy/laravel-darkmode-toggle provides a simple, ready-to-use dark mode toggle for Laravel applications. Add a Blade component, configure its appearance, and let users switch between light and dark themes with their preference persisted across visits. Lightweight and easy to integrate.
Install and scaffold in three commands
composer require jeremykenedy/laravel-darkmode-toggle
php artisan darkmode:install --css=tailwind # tailwind | bootstrap5 | bootstrap4
php artisan vendor:publish --tag=darkmode-js # only if you use Vue/React/Svelte
darkmode:install drops the config file and the CSS variables your chosen framework needs. Verify the result in config/darkmode.php and resources/views/vendor/... before touching markup — most "the toggle does nothing" reports trace back to a css_framework that doesn't match what the app actually loads.
Requirements to check first
composer.json still advertises Laravel 10/11, but Composer will refuse to resolve either branch: every 10.x and 11.x release is affected by CVE-2026-48019 with no patched version. If you are on Laravel 10 or 11, this package is not the thing to fix first — upgrade the framework.composer.lock will keep working, but composer update on those branches will fail resolution. Budget for that before a routine dependency bump.First use case
Drop the Blade component into your layout head and body:
{{-- <head>, before your stylesheet so the init script paints first --}}
<x-darkmode-init />
{{-- wherever the control belongs --}}
<x-darkmode-toggle />
The init script runs inline so the correct theme is applied before first paint; place it above your stylesheets or you'll get a flash of the wrong theme on hard reloads. The toggle itself is self-contained — no Alpine, no Livewire, no JS build step required for the Blade path.
Where to look
config/darkmode.php — every option, and now every option has an env fallback, so you can set DARKMODE_CSS, DARKMODE_FRONTEND, etc. in .env and skip publishing the config entirely for simple apps.resources/lang/en/darkmode.php — all toggle labels live here now (see Gotchas: they were hardcoded until v2.0).Blade-only app (most common)
<x-darkmode-toggle /> in the nav, <x-darkmode-init /> in <head>. Persistence is client-side in localStorage by default. To get the nicest native rendering, enable the two new (off-by-default) options:
// config/darkmode.php
'color_scheme' => true, // mirrors the theme onto CSS `color-scheme`
'sync_across_tabs' => true, // keeps other tabs in step via the `storage` event
color_scheme is the one most people notice: without it, native <select>, date pickers and scrollbars stay light while the rest of the page goes dark.
Bootstrap 5.3 apps
Bootstrap 5.3 has its own theme mechanism (data-bs-theme). Wire the two together:
'css_framework' => 'bootstrap5',
'data_attribute' => 'data-bs-theme',
Leave it null if your Bootstrap 5 setup already drives .dark/.light classes through your own CSS variables — setting both produces two competing sources of truth.
Livewire
The package ships a Livewire component so the choice survives a full page navigation without a flash. For it to persist server-side:
'persist_to_server' => true,
'persist_field' => 'dark_mode', // name it whatever your users table has
Then render the Livewire variant instead of the Blade one. v2.0 fixed two long-standing bugs here: the component ignored persist_field and always read a hardcoded dark_mode column, and it wrote to the profile even with persist_to_server off. If you're on a custom column name, upgrade before you debug your own migration.
Vue / React / Svelte
Publish with --tag=darkmode-js so the components land at the import path the README documents — nothing published them before v2.0, so earlier attempts to follow the README silently produced nothing. New props: dataAttribute, colorScheme, syncAcrossTabs, toggleLabel, labels — use labels to pass translated strings for SSR or when you don't want to publish lang files.
Deploy / CI scripting
darkmode:switch and darkmode:update rewrite the CSS framework and frontend settings. v2.0 made these honest: they now write DARKMODE_CSS / DARKMODE_FRONTEND when those keys exist, because those take precedence over UI_KIT_*. The .env matcher also changed from a substring check to an exact key match — previously APP_UI_KIT_CSS could make a write silently no-op while the command reported success. If you have automation that greps the command output, re-verify it after upgrading; the exit code is now trustworthy where it wasn't before.
Translation
Every label comes from resources/lang/en/darkmode.php. Publish the lang files if you need to override, and don't hardcode label strings in your own wrapper — pass labels to the JS components or publish the lang file for Blade.
composer.json still allows them so existing installs aren't cut off mid-flight, but Composer will not resolve either branch because of CVE-2026-48019 and the absence of a patch. Treat this as a hard floor of Laravel 12 in practice.color_scheme, sync_across_tabs, and data_attribute are inert until you set them. Upgrading gives you a lot of fixes but none of the new behaviour for free.css_framework before v2.0. If you had hand-patched views to compensate, drop those patches or you'll double up.v-click-outside. If your app happens to register a global v-click-outside, that's fine, but the package no longer relies on it — and it threw on mount on any host that didn't have one. Same class of problem: nothing to install, nothing to shim.class_name must not be empty. An empty value made the init script emit classList.toggle(''), which throws. It's guarded now, but if you pass a dynamic class list from config, still fall back to a real value rather than ''.localStorage holding something outside light/dark/system painted one theme then flipped to another. The init script now normalises. If you wrote a third value into that key yourself, stop — it'll be discarded.prefers-color-scheme is now followed in all frontends, not just Blade. On Vue/React/Svelte, System mode used to sit still when the OS theme changed. Good news, but it means a user with System selected will see the page change under them — check your own transitions if you animate theme switches.darkmode:install --css=material now fails. Previously a single flag skipped validation and wrote an unsupported framework. If a scaffold script relied on that (accidentally or not), it will now exit non-zero. That's the correct behaviour; fix the framework name.localStorage access is guarded. A browser with storage blocked used to throw on page load. If you had a try/catch wrapper around the toggle for this reason, it's now redundant.DARKMODE_CSS / DARKMODE_FRONTEND win over UI_KIT_CSS / UI_KIT_FRONTEND. When a setting "won't take", check .env for the DARKMODE_* variant before editing config — and note that APP_UI_KIT_CSS no longer shadows anything now that key matching is exact.aria-haspopup, aria-expanded, role="menu", role="menuitemradio" with aria-checked, Escape returns focus to the trigger, arrows open and move between options, icons are aria-hidden. If you wrapped or re-rendered the toggle, retest keyboard and screen reader flows; you may be dropping attributes.composer validate --strict, composer audit and a real frontend build across PHP 8.2–8.5 and Laravel 12–13. That's a good signal, but it also means the supported matrix is exactly that — if you're on PHP 8.1, you're outside it regardless of what composer.json permits..env writing behaviour and the --css validation both changed in ways that can break scripts. Lock to ^2.0 deliberately rather than letting a floating constraint pull it in mid-release.How can I help you explore Laravel packages today?