awcodes/light-switch
Filament plugin that adds light, dark, and system theme switching to panel auth pages. Configure the switcher's position with Alignment enums and control which routes display it via enabledOn(). Supports Filament 3, 4, and 5.
1. Check the prerequisite before anything else. The plugin only works if your panel already builds its own Tailwind theme. If you have never run php artisan filament:theme (or hand-created resources/css/filament/{panel}/theme.css and registered it with ->viteTheme(...)), stop and do that first. Installing Light Switch on a stock, non-custom-theme panel succeeds silently and renders nothing useful.
2. Install and pin the major that matches your Filament version.
composer require awcodes/light-switch
| Package | Filament |
|---|---|
| 1.x | 3.x |
| 2.x | 4.x |
| 3.x | 4.x & 5.x |
Composer will usually resolve correctly, but assert it in composer.json if you run Filament 4 or 5 in production — a stray ^1.0 constraint against a Filament 4 app is an avoidable afternoon.
3. Add the views to your theme CSS. This is the step people skip:
/* resources/css/filament/admin/theme.css */
@source '../../../../vendor/awcodes/light-switch/resources/**/*.blade.php';
4. Register the plugin in the panel provider:
use Awcodes\LightSwitch\LightSwitchPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
LightSwitchPlugin::make(),
]);
}
5. Build and clear.
npm run build
php artisan filament:assets
php artisan optimize:clear
6. First use case: verify on /admin/login. Open the login page in a private window, toggle dark, hard-reload, and confirm the choice sticks. Then log in and confirm the authenticated panel agrees with the choice you made pre-login. Those two checks cover ~90% of the integration.
Where to look first when something is off:
vendor/awcodes/light-switch/resources/views/ — the actual Blade markup; if your HTML doesn't contain it, the plugin isn't rendering at all (gating problem, not styling problem).vendor/awcodes/light-switch/src/Enums/Alignment.php — the authoritative list of positions; only Alignment::BottomCenter is documented by name, so read the enum rather than guessing case names.vendor/awcodes/light-switch/src/LightSwitchPlugin.php — the whole public API is two methods; reading it takes two minutes and removes all ambiguity.php artisan route:list --name=auth — the exact route names your enabledOn() strings must match.The default route list has changed repeatedly across releases (auth.email removed in v1.0.2, the profile view removed in v2.0.1). Treat the default as an undocumented implementation detail and set it yourself:
LightSwitchPlugin::make()
->enabledOn([
'auth.login',
'auth.password',
'auth.register',
]),
Note that enabledOn() replaces the defaults rather than extending them. Anything you want must be listed.
Route matching uses Str::contains() against the current route name, so partial fragments are the idiomatic way to scope per panel:
// Admin panel provider
LightSwitchPlugin::make()->enabledOn(['admin.auth.login', 'admin.auth.password']),
// Customer panel provider
LightSwitchPlugin::make()->enabledOn(['app.auth.login', 'app.auth.register', 'app.auth.password']),
Each panel's plugin instance is independent, so position and scoping can differ between the back office and the customer-facing app. This is where the package earns its keep — a customer-facing login page is a first impression; an internal admin login is not.
Default is top-right. When your auth layout is heavily customized (MFA steps, SSO button blocks, legal footers), reposition rather than fight the default:
use Awcodes\LightSwitch\Enums\Alignment;
LightSwitchPlugin::make()->position(Alignment::BottomCenter),
Treat the position as part of the auth design review, not as a preference — check it at mobile and desktop breakpoints on every auth screen you enabled, not just login.
defaultThemeModeThe switcher reads Filament's defaultThemeMode for its initial state (fixed in v2.0.2). If you set ->defaultThemeMode(ThemeMode::Dark) on the panel, verify the switcher actually reflects that on first paint — otherwise you get a visible flip on load, which defeats the entire purpose.
Because the plugin ships Blade views that are Tailwind-scanned at build time, any change to your theme CSS or to a published/overridden view requires a rebuild. Keep npm run dev running alongside php artisan serve, and remember that php artisan serve alone will not pick up CSS changes.
Ship it behind your normal release process, not a separate one — the revert is removing one line from plugins() plus a rebuild. If you want a staged rollout, gate the plugin registration on a config/env flag at the panel-provider level:
->plugins(array_filter([
config('features.auth_theme_switch') ? LightSwitchPlugin::make()->enabledOn([...]) : null,
]))
Silent failure without a custom theme. This is the number one trap. Install + register + no @source line = no error, no switcher. Before you accept a "doesn't work" bug report, grep your theme CSS for the vendor path.
@source is Tailwind v4 syntax. Filament 4/5 themes use it. If you are on Filament 3 (package 1.x) with a Tailwind v3 theme, the equivalent is adding the vendor glob to your tailwind.config.js content array instead. Confirm which Tailwind major your theme compiles with before pasting the README snippet verbatim.
enabledOn() replaces, it does not merge. Passing ['auth.login'] removes password reset and registration from the switcher. If the switcher "disappeared from the register page," this is why.
Str::contains() over-matches. 'auth.login' will match any route name containing that substring. Broad strings like 'auth' will light up every auth page in the panel including ones you didn't consider (2FA challenge, email verification, SSO callbacks). Verify with php artisan route:list --name=auth and prefer the most specific fragment that still achieves your intent.
Upstream default drift is a silent production change. Because defaults have changed between patch/minor releases, a composer update can quietly alter which pages show the switcher. Always set enabledOn() explicitly, and read the changelog on every bump.
Z-index collisions recur. v1.0.3 dropped the switcher to z-index: 40 specifically to stop it covering Filament notifications. That class of bug comes back the moment someone customizes the auth layout. If you see the toggle covering a toast, a validation banner, or an MFA prompt, this is the cause — and the fix belongs in your theme CSS override, not in a fork if you can help it.
Theme flash is the whole point. The pitch is "no bright flash for dark-mode users." Test on a throttled connection, cold cache, with dark mode preferred at the OS level. If you see a white flash before the theme applies, you have undercut the reason for installing the package — investigate whether the switcher's inline init runs before Filament's own theme boot.
Preference persistence is client-side only. There is no per-user database column, no migration, and no server-side preference. Scope and limitations follow from that: a user on a different browser or device starts fresh, and clearing storage resets them. Do not file a bug for it, and do not promise per-user persistence in a spec.
Check whether the preference carries across the login boundary. It may or may not, depending on how Filament's own panel theme bootstrapping runs. Test it explicitly — a flip right after sign-in is jarring and is the second most common complaint after the missing-custom-theme issue.
No translations, no custom palettes, no per-panel token overrides. The package is intentionally narrow. If a stakeholder asks for branded theme colors or localized labels mid-sprint, that is a scope change, not a config flag — log it as an explicit non-goal up front.
Extension points to check rather than assume. If the service provider registers publish tags, php artisan vendor:publish --provider="Awcodes\LightSwitch\LightSwitchServiceProvider" will let you override the Blade views. Read src/LightSwitchServiceProvider.php first; if there are no publish tags, override by CSS or fork. The codebase is small enough that a fork-and-patch is an hours-scale cost, which is the real reason a 48-star, single-maintainer dependency is acceptable here.
Debugging checklist, in order:
enabledOn() gating.@source line or stale asset build.position() and layout/z-index conflicts.defaultThemeMode mismatch.npm run build wasn't run, or assets cached (php artisan filament:assets, optimize:clear).Upgrade runbook entry. Every Filament major bump likely requires a Light Switch major bump. Add it to the panel upgrade checklist alongside filament/filament so it isn't discovered by a broken login page.
How can I help you explore Laravel packages today?