silasrm/filament-themes

Complete, ready-made panel themes for Filament with a live-preview switcher and flexible persistence (per-user, global, per-tenant).

Maintainers

Package info

github.com/silasrm/filament-themes

pkg:composer/silasrm/filament-themes

Transparency log

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-07-18 13:08 UTC

This package is auto-updated.

Last update: 2026-07-18 13:09:25 UTC


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 (set global_store to session or database in config).
  • perTenant(Closure $get, Closure $set) — delegate to your tenant model. $get receives the resolved tenant (or null) and must return a ?string theme key; $set receives the chosen string key.
  • tenantResolver(Closure $resolver) — resolve the tenant model when Filament has not bound it yet (e.g. the login screen of a domain-based tenant). $resolver receives the Request and returns the tenant model (or null). 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:

  1. Accent colours — on each request the middleware registers the active theme's primary/secondary/gray with 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.
  2. Surface restyle — a <style> block is injected at PanelsRenderHook::HEAD_END with CSS generated from the active theme, targeting Filament's fi-* classes (background, sidebar, topbar, cards, tables, inputs, text, borders). The rules use !important so 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.