Killing @if($isTailwind) in Blade: a theme-class seam for Livewire tables
The Problem
The package ships Tailwind and Bootstrap themes. Every Blade that needed styling did this:
@class([
'px-3 py-2 ...' => $isTailwind,
'p-2 border ...' => $isBootstrap,
])
Fine for two themes. But I was adding a Flux theme, and the honest question was: do I really want a third @elseif in every one of ~40 Blade partials? That's the smell. When adding a variant means editing every template, your variation axis is in the wrong place.
Analogy: this is a light switch wired directly to the bulb. Want a dimmer? You rewire every room. Better to run everything through one panel.
The Seam
One static class holds per-theme class strings, keyed by a dotted name:
class ThemeStyles
{
protected static array $classes = [
'tailwind' => [
'table.wrapper' => 'shadow overflow-y-auto border-b ... sm:rounded-lg',
// ...
],
'flux' => [
/* only the keys Flux overrides */
],
];
public static function for(string $theme, string $key): string
{
/* ... */
}
}
Blades stop branching and just ask for a key:
{{-- component trait: themeClasses() delegates to ThemeStyles::for() --}}
<td @class([$this->themeClasses('td.collapsed.base')])>
The trait behind it is three lines:
public function themeClasses(string $key): string
{
return ThemeStyles::for($this->getTheme(), $key);
}
Two Design Choices Matter Here
Flux falls back to Tailwind. Flux is Tailwind-based - it only differs on a handful of keys (dropdown panels, pills, empty state). So for() resolves the theme's key, and if it's missing, falls back to the tailwind map. A new theme defines only its diffs, not the whole surface.
No closures in the map. Tempting to store fn () => ... for dynamic classes. Don't - closures can't be serialized, so php artisan config:cache (and Octane) chokes. Plain strings keep it cache-safe. Anything genuinely dynamic stays in the Blade around the seam, not inside the map.
| Before | After | |
|---|---|---|
| Add a theme | Edit ~40 Blades | Add one map entry, override diffs only |
| Class source | Scattered inline | One file |
| Cache-safe | n/a | Yes (no closures) |
| Blade job | Branch per theme | Ask for a key |
Don't Trust Yourself - Guard with Characterization Tests
Migrating 40 templates by hand is exactly where you silently drop a class and shift a border 1px. So before touching a Blade, I pinned its current output:
it('renders the collapsed cell identically after the seam migration', function () {
$html = BooleanColumn::make('Active')
->render(/* ... */)
->toHtml();
expect($html)->toContain('p-3 table-cell text-center');
});
A characterization test doesn't care whether the output is good - only that it didn't change. Migrate one Blade group, run the per-theme visual suite, confirm green, commit, next group. One slice at a time.
One real find along the way: an empty-string class key rendered subtly differently through @class() than an inlined blank - the kind of thing you only catch because the test compares exact output, not intent.
Takeaway
When adding a variant forces you to edit every file, extract the variation into a seam the files consult. For a Blade UI package that's a keyed class map plus a one-line themeClasses() accessor. Keep the map serializable so it survives config caching, and let characterization tests carry the risk of a large mechanical migration.
The payoff: the Flux theme landed as a set of key overrides, not a fourth copy of every template.
Comments
No comments yet. Start the discussion.