Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Darkmode Toggle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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

  • PHP 8.2+ and Laravel 12/13 for a clean install.
  • 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.
  • Laravel 11.x/10.x existing installs that are already locked in 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

  1. 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.
  2. resources/lang/en/darkmode.php — all toggle labels live here now (see Gotchas: they were hardcoded until v2.0).
  3. The package views, to see which markup your framework variant renders.

Implementation Patterns

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.

Gotchas and Tips

  • Laravel 10/11 are dead ends. 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.
  • All three new config options ship off. 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.
  • Bootstrap + Livewire users must upgrade. The Livewire toggle rendered Tailwind markup regardless of css_framework before v2.0. If you had hand-patched views to compensate, drop those patches or you'll double up.
  • The Vue component no longer needs 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 ''.
  • Bad stored preferences are now sanitised. Before, 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.
  • Env precedence bites. 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.
  • Label overrides belong in the lang file, not in a view override. View overrides will drift from the ARIA markup in v2.0 (see below); the lang file is the supported seam.
  • Accessibility is now contract-level. The toggle implements the ARIA menu button pattern — 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.
  • CI is stricter than your app. The package now runs PHPStan level 6, 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.
  • Pin your version. This is a major bump that is backwards compatible by contract (class names, config keys, view names, publish tags and command signatures unchanged), but the .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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels