Killing @if($isTailwind) in Blade: a theme-class seam for Livewire tables

php dev.to

TL;DR

  • Blade templates that branch @if($isTailwind) ... @elseif($isBootstrap) for CSS classes don't scale. Adding a Flux theme meant a third branch in every file.
  • I moved all class strings into one static map (ThemeStyles) and gave Blades a single seam: themeClasses('key'). Themes now override keys, not re-implement templates.
  • Kept it closure-free so it survives config:cache, and guarded every migration with characterization tests that assert the rendered HTML is byte-for-byte unchanged.

This is from the public cleaniquecoders/laravel-livewire-tables v4 fork.

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,
])
Enter fullscreen mode Exit fullscreen mode

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 { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Blades stop branching and just ask for a key:

{{-- component trait: themeClasses() delegates to ThemeStyles::for() --}}
<td @class([$this->themeClasses('td.collapsed.base')])>
Enter fullscreen mode Exit fullscreen mode

The trait behind it is three lines:

public function themeClasses(string $key): string
{
    return ThemeStyles::for($this->getTheme(), $key);
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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.

Source: dev.to

arrow_back Back to Tutorials