silasrm / filament-themes
Complete, ready-made panel themes for Filament with a live-preview switcher and flexible persistence (per-user, global, per-tenant).
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
README
Ready-made complete panel themes for Filament — not just accent colours, but a full restyle of background, sidebar, topbar, cards, tables, inputs, text and borders. Ships a live-preview switcher and flexible persistence (per-user, global, per-tenant).
Supports Filament 4 & 5, Laravel 11 / 12 / 13, PHP 8.2+.
Installation
composer require silasrm/filament-themes
The package's service provider is auto-discovered, which also registers the
filament-themes view namespace and translations. If you have disabled
package auto-discovery, register it manually in bootstrap/providers.php:
use Silasrm\FilamentThemes\FilamentThemesServiceProvider; return [ // ... FilamentThemesServiceProvider::class, ];
Optionally publish the config to add or override themes:
php artisan vendor:publish --tag=filament-themes-config
Usage
Register the plugin on your panel. With no configuration it works out of the box using a cache-backed global store:
use Silasrm\FilamentThemes\ThemesPlugin; public function panel(Panel $panel): Panel { return $panel->plugin(ThemesPlugin::make()); }
A Theme page appears in the panel with a grid of theme swatches — hover to preview instantly, click to apply. The applied theme is re-applied on every request via a render hook, so a full page reload keeps the chosen look.
Persistence modes
Combine any of the three. Resolution cascades user → tenant → global → default. The first store that returns a known theme key wins.
use Filament\Facades\Filament; use Silasrm\FilamentThemes\ThemesPlugin; ThemesPlugin::make() ->perUser() // each user picks their own (stored on the user model) ->global() // app-wide default (cache-backed, no migration) ->perTenant( get: fn (): ?string => Filament::getTenant()?->branding_settings['theme'] ?? null, set: fn (string $key): void => Filament::getTenant()?->update(['branding_settings' => [...]]), ) ->defaultTheme('azul');
perUser(?string $attribute = 'theme')— reads/writes an attribute on the authenticated user. Falls back to session for guests.global(?ThemeStore $store = null)— app-wide theme. Defaults to a cache store (setglobal_storetosessionordatabasein config).perTenant(Closure $get, Closure $set)— delegate to your tenant model.$getreceives the resolved tenant (ornull) and must return a?stringtheme key;$setreceives the chosenstringkey.tenantResolver(Closure $resolver)— resolve the tenant model when Filament has not bound it yet (e.g. the login screen of a domain-based tenant).$resolverreceives theRequestand returns the tenant model (ornull). See Domain-based tenancy & login.defaultTheme(string $key)— fallback key used when nothing is selected.
Multi-panel / multi-tenant
Each panel builds its own resolver from the stores you configured on that
panel. This avoids a shared singleton being overwritten by the last-booted
panel, so e.g. an admin and a member panel can each resolve their own
tenant's theme independently. If you need a resolver outside the plugin (e.g. in
a custom controller), build one bound to the current panel:
$plugin = Filament::getCurrentPanel()->getPlugin('filament-themes'); $resolver = $plugin->makeResolver(app(\Silasrm\FilamentThemes\ThemeManager::class)); $resolver->resolveKey(); // current theme key for this panel
Forcing light/dark mode
By default the plugin forces the panel's colour mode to follow the resolved
theme's mode (light/dark). This prevents Filament's built-in dark mode
from flipping its --gray-* scale (including form labels) while the theme's
surface colours stay fixed — which would otherwise yield invisible
white-on-white labels.
If you would rather let the visitor's OS/browser preference win, disable it:
ThemesPlugin::make()->forceMode(false);
Custom themes
use Silasrm\FilamentThemes\Theme; ThemesPlugin::make()->themes([ Theme::make('minha-marca', 'Minha Marca') ->primary('#ff0055') ->secondary('#7c3aed') ->gray('#64748b') ->surfaces( bg: '#faf5ff', surface: '#ffffff', sidebar: '#f3e8ff', topbar: '#ffffff', text: '#2e1065', muted: '#6d6591', border: '#e9d5ff', ), Theme::make('minha-dark', 'Minha Dark') ->dark() ->primary('#22d3ee') ->surfaces( bg: '#0b1220', surface: '#111827', sidebar: '#0b1220', topbar: '#0b1220', text: '#e5e7eb', muted: '#9ca3af', border: '#1f2937', ), ]);
Theme fluent API:
| Method | Purpose |
|---|---|
Theme::make(string $key, string $label) |
Create a theme (key is used for storage & CSS scoping) |
->label(string) |
Display name in the switcher |
->mode('light'|'dark') / ->dark() |
Colour mode |
->primary(string) / ->secondary(string) / ->gray(string) |
Accent colours (any CSS colour) |
->surfaces(bg:, surface:, sidebar:, topbar:, text:, muted:, border:) |
Surface colours |
You can also add themes declaratively in config/filament-themes.php under
presets (same shape as the built-ins).
Domain-based tenancy & login
For path-based tenancy (tenant(Team::class)) the tenant is exposed as a
route parameter, so the theme resolves automatically — even on the login
screen.
For domain-based tenancy (tenantDomain('{tenant}.example.com')) the
tenant lives in the request host, not a route parameter. Filament has not
bound the tenant yet on the login / register screen (Filament::getTenant()
is null), so the per-tenant store would otherwise fall back to the global /
default theme. Provide a tenantResolver so the package can find the tenant
from the host and apply its theme before authentication:
use App\Models\Unit; use Filament\Facades\Filament; use Illuminate\Http\Request; use Silasrm\FilamentThemes\ThemesPlugin; ThemesPlugin::make() ->perTenant( get: fn (?Unit $tenant): ?string => $tenant?->branding_settings['theme'] ?? null, set: fn (string $key): void => Filament::getTenant()?->update(['branding_settings' => [...]]), ) ->tenantResolver( fn (Request $request): ?Unit => Unit::where('domain', $request->getHost())->first(), );
The tenantResolver is tried after Filament::getTenant() and before the
{tenant} route parameter, so it only kicks in when Filament has not yet
bound the tenant (login, register, etc.).
Disable the switcher page
ThemesPlugin::make()->switcherPage(false);
Configuration
After publishing (--tag=filament-themes-config), config/filament-themes.php
supports:
| Key | Default | Purpose |
|---|---|---|
default |
azul |
Fallback theme key |
global_store |
cache |
Store used by global() mode — cache, session or database |
cache_key |
filament-themes.global |
Cache key for the global store |
session_key |
filament-themes.theme |
Session key for the global/session store |
user_attribute |
theme |
Column/attribute used by perUser() |
presets |
(many) | Theme definitions available in the switcher |
Built-in themes
Light: Azul, Verde, Oceano, Laranja, Rosa, Lavanda, Ouro, Nord (claro), Sunset Pastel: Menta, Lavanda, Pêssego, Céu, Rosa, Amarelo, Sálvia Dark: Dracula, Nord, Meia-noite, Floresta escura, Grafite, Vinho
How it works
The plugin styles the panel in two complementary ways:
- Accent colours — on each request the middleware registers the active
theme's
primary/secondary/graywith Filament's native colour system ($panel->colors()+FilamentColor::register()). Filament then generates the matching CSS variables (--primary-500, etc.) used across buttons, badges, links and focus rings. - Surface restyle — a
<style>block is injected atPanelsRenderHook::HEAD_ENDwith CSS generated from the active theme, targeting Filament'sfi-*classes (background, sidebar, topbar, cards, tables, inputs, text, borders). The rules use!importantso they win over Filament's own utility/component CSS — including the styles Vite injects at runtime in development (npm run dev), which would otherwise override a plain-specificity overlay.
It is a pure CSS overlay — no build step or asset compilation required.
Compatibility note: because it targets
fi-*classes, a Filament minor release could change selectors. The CI matrix covers Filament 4 & 5; CSS output is snapshot-tested.
Testing
composer test
composer analyse
composer lint
License
MIT. See LICENSE.md.