Filament Bladewind
#Filament BladeWind
Per-page CSS for Filament panels, built on daikazu/bladewind.
A Filament panel theme is roughly 650 KB, and about 90% of it is the fi-* component rules. On a
Filament 4/5 (Tailwind 4) theme, BladeWind on its own only splits Tailwind's utilities per page and
keeps components in the shared root, so on a panel it saves almost nothing. This plugin splits the
components too, and keeps every page styled after Livewire updates:
- Components are split per page. Each page loads a small shared root plus the
fi-*components and utilities it actually uses, and both layers keep their cascade position. - Livewire updates get their missing CSS. Filament renders action modals, forms and
notifications on demand. When an update's HTML needs rules the page didn't load, they're added
to the response and put in
<head>before Livewire morphs the DOM, so nothing paints unstyled. The rules are re-emitted in stylesheet order, so a streamed rule can't win the cascade out of order. - Classes added by Filament's JavaScript are always loaded. Every
fi-*class that appears as a string literal in Filament's published JS (added withclassList) goes into every page.
Measured on a real panel (theme ~657 KB):
| Page | Loaded CSS | Saving |
|---|---|---|
| Login | 181 KB | 72% |
| Dashboard | 259 KB | 61% |
| Create form | 288 KB | 56% |
| Resource table | 398 KB | 39% |
[!WARNING] Experimental. This plugin builds on BladeWind internals (0.1.x, pinned to that minor) and on how Livewire and Filament ship updates. Test your panel's pages, modals and actions before you use it in production. The measurements above and the end-to-end modal test come from a Filament 5 panel; the 1.x and 2.x branches are covered by unit tests only.
#Compatibility
| Branch | Filament | Livewire | Tailwind (theme) | Laravel | PHP | Package version |
|---|---|---|---|---|---|---|
| 1.x | 3.3 | 3.x | 3 | 13.x | 8.4+ | ^1.0 |
| 2.x | 4.x | 3.x | 4 | 13.x | 8.4+ | ^2.0 |
| 3.x | 5.x | 4.x | 4 | 13.x | 8.4+ | ^3.0 |
BladeWind requires Laravel 13 and PHP 8.4 on every branch.
- Tailwind 4 themes (2.x, 3.x): the driver moves
@layer componentsinto the per-page pool, and each page file re-opens the components and utilities layers. - Tailwind 3 themes (1.x): the output has no cascade layers, so BladeWind's flat split already
pools everything from the first class rule on, the
fi-*components included.
#Installation
composer require jeffersongoncalves/filament-bladewind
#Usage
Replace ->viteTheme() with the plugin, passing it the same entry:
use JeffersonGoncalves\Filament\BladeWind\BladeWindPlugin; $panel // ->viteTheme('resources/css/filament/admin/theme.css') ->plugins([ BladeWindPlugin::make() ->theme('resources/css/filament/admin/theme.css'), ]);
Build your assets as usual (npm run build / bun run build). The web server needs write access
to public/bladewind, where BladeWind writes the page files. Add that directory to .gitignore.
#Options
BladeWindPlugin::make() ->theme('resources/css/filament/admin/theme.css') // Views of third-party plugins to analyse (vendor/filament/* and resources/views are always in). ->paths([base_path('vendor/acme/filament-widgets/resources/views')]) // Classes every page carries: exact names or `prefix-*` patterns. ->safelist(['fi-custom-*']) // How long (seconds) a loaded page's class set is kept for its Livewire updates. ->ttl(43200);
The public site of the same app can keep using BladeWind with its own stylesheet
(bladewind.stylesheets in config/bladewind.php). Panel requests switch that setting to the
panel theme for the duration of the request.
#How it works
- Theme. The plugin sets the panel theme to BladeWind's
@bladewindStyleslinks. A persistent panel middleware points BladeWind at the panel theme, the panel's views andvendor/filament/*, and at this package'sfilamentCSS driver. - Split. The
fi-*components go into the per-page pool next to the utilities. The support declarations (theme variables,--tw-*properties,@property,@keyframes) stay whole in the root, which is a few KB. Because the root has every variable, streamed rules never depend on one the page didn't load. - Page load. The middleware records the page's class set in the cache under a random id and
puts the id in a
<meta>tag. - Livewire update. A small script sends that id with every Livewire request. A Livewire
responselistener reads the HTML the update renders and computes the rules the page doesn't have yet. The HTML is the component HTML, plus thepartialsandislandsthat Livewire 4 uses (Filament 5 sends action modals as a partial). The listener returns the rules ineffects.bladewind, and the script adds them to<head>before the morph. On Livewire 4 that happens ininterceptMessage→onSuccess; on Livewire 3 it happens in thepayload.intercepthook.
If the cache has lost a page's id, the next update simply re-sends everything its fragment needs. It never sends less.
#Diagnostics you will see
BladeWind reports these codes for a panel, and they're expected:
- BW1001: a view uses
@include($name). Filament renders components through dynamic includes; their HTML is covered when it renders. - BW2002: an Alpine binding can't be enumerated.
- BW2004: a
@classentry isn't a string. - BW6005: the support layers are kept whole, which the Tailwind 4 driver does on purpose (2.x, 3.x).
#Testing your panel
BladeWind's AssertsPageStyles trait works on panel pages as well:
use Daikazu\BladeWind\Testing\AssertsPageStyles; use Daikazu\BladeWind\Testing\PageExpectation; uses(AssertsPageStyles::class); it('styles the users table', function () { $this->actingAs($admin, 'admin'); $this->assertPageStyles('/admin/users', new PageExpectation( framework: 'filament', diagnostics: ['BW1001', 'BW2002', 'BW2004', 'BW6005'], // no BW6005 on 1.x )); });
#Testing
composer test
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Credits
#License
The MIT License (MIT). Please see License File for more information.