What I do
- Restyle a token with zero code via
config('livewire-powergrid.theme_overrides') - Create a new theme class extending
Themewith per-section token methods (layout(),header(),table(),footer(),cols(),tabs(), plusfilter(),editable(),toggleable()) - Author a data-first theme as a plain array with
ArrayTheme(fromArray()/fromFile()or a subclass) - Update an existing theme by adding or overriding tokens in the relevant section method
- Wire per-component overrides via
template()+merge()(patch tokens) - Register a theme by name (
PowerGridManager::registerTheme()) and select it by name in config - Run the theme test suite after changes
When to use me
Use this when:
- A new UI theme needs to be added (e.g. Bootstrap, Flowbite, ShadCN)
- An existing theme's tokens need to be changed or extended
- A few token classes need to change with no theme class at all (config
theme_overrides) - A single PowerGrid component needs a one-off theme override
- Token keys are missing or misnamed and produce empty class output
This skill is only about creating and updating v7 themes. It is self-contained — everything you need is here and in REFERENCE.md (same folder).
How to use me
Example 1: Create a new theme class
Use the 'powergrid-theme' skill to create a new theme for Bootstrap 5.
Theme class: src/Themes/Bootstrap.php
Example 2: Update tokens in an existing theme
Use the 'powergrid-theme' skill to update the DaisyUI theme:
- Change table.layout.thead to use 'bg-neutral text-neutral-content'
- Add a filter.boolean.select override for DaisyUI input sizing
Example 3: No-code override via config
Use the 'powergrid-theme' skill to make table headers bolder for every table
via config('livewire-powergrid.theme_overrides'), without a Theme class.
Example 4: Per-component override
Use the 'powergrid-theme' skill to apply a partial theme override to
MyTableComponent so that table.layout.tr gets an extra 'stripe' class.
How it fits together (the short version)
A theme is a PHP class in src/Themes/. It extends Theme (src/Themes/Theme.php) and returns a token map. Blade views read tokens with the theme() / theme_view() helpers, e.g. {{ theme('table.layout.td') }} and {{ theme_view('pagination') }}.
There are three ways to change theming, cheapest first:
- No-code overrides (
config('livewire-powergrid.theme_overrides')). A nested token array inresources/config/livewire-powergrid.php, merged last (highest precedence) inresolveTokens(). Restyle any token without touching a Theme class:'theme_overrides' => [ 'table' => ['layout' => ['th' => 'font-bold px-4 py-3']], ], - Section methods on a Theme class. Each token group is its own public method returning that group's slice —
layout(),header(),table(),footer(),cols(),tabs(), plusfilter(),editable(),toggleable().Theme::themeTokenMethods()lists them andresolveTokens()auto-merges each. ArrayTheme— a data-first theme authored as a plain nested array (no builder). See below.
struct() is now tiny. It only sets the base view; it does not carry the whole token tree any more:
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string
{
return 'livewire-powergrid::components.themes.tailwind';
}
Everything else lives in the section methods. A section method can be written two equivalent ways:
- Plain nested array (6.x-familiar):
public function footer(): array { return ['footer' => ['pagination' => ['item' => 'btn ...']]]; } - Fluent + type-safe via the
section()helper onTheme(it returns['footer' => [...]]withview*tokens baseView-prefixed):public function footer(): array { return $this->section('footer', fn (Components\Footer $f) => $f ->layout(fn (Components\Layout $l) => $l->container('...')->select('...')) ->pagination('pagination')); }
The three canonical themes are the patterns to copy:
src/Themes/Tailwind.php— the base theme; full section methods;parentTheme = nullsrc/Themes/DaisyUI.php— a token-only subclass (parentTheme = Tailwind::class) that ships zero blades — imitate thissrc/Themes/Flux.php— a subclass that keeps its own<flux:*>blades only where the HTML genuinely differs
Inheritance is explicit: Theme::$parentTheme defaults to null. Set protected ?string $parentTheme = Tailwind::class; and a child overrides only the sections it changes — every other token, view, filter, editable, or toggleable value falls through to Tailwind. (DaisyUI and Flux both do this.)
Prefer tokens over new blades. DaisyUI ships no Blade files at all: resources/views/components/themes/daisyui/ no longer exists. When a token names a view the theme does not ship, Theme::doResolveView() inherits the parent's blade. Only add a Blade file when the markup is genuinely different (as Flux does for its <flux:*> components).
Selecting a theme. config('livewire-powergrid.theme') accepts a registered name ('tailwind', 'daisyui', 'flux') or an FQCN. Names live in PowerGridManager::$themes (default DEFAULT_THEMES); register your own with PowerGridManager::registerTheme('bootstrap', BootstrapTheme::class) and PowerGridManager::resolveThemeClass() resolves it.
Full token maps, the builder API, and the resolution order are in REFERENCE.md.
Workflow
- Identify the goal. A few token classes (no-code), a new/updated theme class, an array theme, or a per-component override?
- Read the closest real theme —
Tailwind.php(the full, authoritative section methods and token surface),DaisyUI.php(a token-only subclass),Flux.php(a subclass with a few of its own blades) — and, if updating, the target file. Do not hand-write a token list from memory; diff againstTailwind.php. - Make the change:
- A few classes only → add them to
config('livewire-powergrid.theme_overrides')(no class needed). - New theme → create
src/Themes/MyTheme.phpfrom the template below (tinystruct()+ section methods). - Token change → edit the relevant section method (
layout(),header(),table(),footer(),cols(),tabs(),filter(),editable(),toggleable()). - Data-first theme → use
ArrayTheme(fromArray()/fromFile()or a subclass). - Per-component → use
template()+merge()(see below).
- A few classes only → add them to
- Register it (new default theme only) in
resources/config/livewire-powergrid.php, by name or FQCN:'theme' => 'my-theme', // registered via PowerGridManager::registerTheme('my-theme', MyTheme::class) // 'theme' => \PowerComponents\LivewirePowerGrid\Themes\MyTheme::class, // FQCN also works - Test:
composer test -- --filter="ThemeTest|ThemeBuilderTest|PowerGridComponentThemeTest" composer test
Minimal new-theme template
Copy this, rename the class, point baseView() at your own view folder, and fill the classes in the section methods. It inherits Tailwind's every undeclared token, view, filter(), editable(), and toggleable() — override a section only where your framework differs. Ship a Blade file only for markup that genuinely differs; otherwise omit ->view() and inherit Tailwind's blade (DaisyUI ships none).
<?php
namespace PowerComponents\LivewirePowerGrid\Themes;
class MyTheme extends Theme
{
// Inherit Tailwind for everything not declared below.
protected ?string $parentTheme = Tailwind::class;
// struct() only carries the base view. Every token group is its own method.
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string
{
return 'livewire-powergrid::components.themes.my-theme';
}
/** @return array<string, mixed> */
public function layout(): array
{
return $this->section('layout', fn (Components\Layout $layout) => $layout
->wrapper('space-y-4')
->card('rounded-xl border')
);
}
/** @return array<string, mixed> */
public function table(): array
{
return $this->section('table', fn (Components\Table $table) => $table
->layout(fn (Components\Layout $layout) => $layout
->container('overflow-x-auto relative border-t')
->table('min-w-full')
->thead('bg-zinc-100')
->tr('border-b')
->th('px-3 py-3 text-left text-xs')
->thActions('px-3 py-3 text-end text-xs')
->tbody('')
->td('px-3 py-2')
->tdActions('px-3 py-2 text-end')
)
->checkbox(fn (Components\Checkbox $checkbox) => $checkbox
->input('h-4 w-4')
)
->radio(fn (Components\Radio $radio) => $radio
->input('rounded-full')
)
);
}
/** @return array<string, mixed> */
public function tabs(): array
{
return $this->section('tabs', fn (Components\Tabs $tabs) => $tabs
->list('inline-flex items-center gap-1 rounded-xl border p-1')
->tab('rounded-lg px-3 py-1.5 text-sm font-medium')
->tabActive('bg-zinc-100 text-zinc-900')
->tabInactive('text-zinc-500')
->badge('rounded-full px-2 py-0.5 text-xs font-semibold')
->badgeActive('bg-blue-100 text-blue-700')
->badgeInactive('bg-zinc-100 text-zinc-600')
);
}
// header(), footer(), cols(), filter(), editable(), toggleable() are optional here —
// declare only the sections that differ from Tailwind. Read Tailwind.php for the full surface.
}
You can equally return plain nested arrays from any of these methods instead of using section(), e.g. public function tabs(): array { return ['tabs' => ['list' => '...', 'tab' => '...']]; }.
ArrayTheme (data-first)
src/Themes/ArrayTheme.php lets you author a theme as a plain nested token array — no builder. Everything not declared falls back to the parent theme (Tailwind by default).
// Ad-hoc, from an array:
$theme = \PowerComponents\LivewirePowerGrid\Themes\ArrayTheme::fromArray(
['footer' => ['pagination' => ['item' => 'btn ...']]],
parentTheme: \PowerComponents\LivewirePowerGrid\Themes\Tailwind::class,
name: 'my-theme',
);
// Or from a PHP file that `return`s the token array:
$theme = \PowerComponents\LivewirePowerGrid\Themes\ArrayTheme::fromFile(base_path('themes/my-theme.php'));
// Or as a subclass:
class MyArrayTheme extends ArrayTheme
{
protected ?string $parentTheme = Tailwind::class;
public function struct(): array
{
return ['footer' => ['pagination' => ['item' => 'btn ...']]];
}
}
Per-component overrides
Both hooks live on your Livewire PowerGrid component (see src/Concerns/Base.php and src/PowerGridComponent.php), and are applied in boot() — never bind powergrid.theme yourself.
Swap the whole theme class for one component by returning it from template(). customThemeClass() is deprecated.
public function template(): ?Theme
{
return new MyTheme();
}
Patch a few tokens for one component (deep merge, so pass only the keys you change):
public function template(): ?Theme
{
return Tailwind::make()->merge([
'table' => [
'layout' => [
'tr' => 'stripe hover:bg-yellow-50',
],
],
]);
}
Rules
struct()only setsbaseView. It returnsComponents\ThemeBuilder::make($this->name())->baseView($this->baseView())(or a plain array forArrayTheme). It no longer carries the whole token tree — every token group lives in its own section method.- Token groups are public methods.
layout(),header(),table(),footer(),cols(),tabs(),filter(),editable(),toggleable()— the list inTheme::themeTokenMethods(). Each returns its slice (['<group>' => [...]]), built either with$this->section('<group>', fn (...) => ...)or as a plain nested array.resolveTokens()merges them automatically. - Declare
parentThemeexplicitly.Theme::$parentThemedefaults tonull; set it toTailwind::classso undeclared sections/tokens inherit from the base theme. A child overrides only the sections it changes. - No-code overrides win.
config('livewire-powergrid.theme_overrides')is merged last inresolveTokens()(afterstruct(), the parent theme, the section methods, and plugin tokens), so it overrides everything. - Correct closure type-hints. Inside
section()the sub-builders are distinct classes:->searchBox(fn (Components\SearchBox $s) => ...),->checkbox(fn (Components\Checkbox $c) => ...),->radio(fn (Components\Radio $r) => ...),->body(fn (Components\Body $b) => $b->tr(fn (Components\Tr $tr) => ...)),->cols(fn (Components\Cols $c) => ...),->tabs(fn (Components\Tabs $t) => ...). Hinting these asComponents\Componentthrows aTypeError. - Pagination is a string.
->pagination('pagination')—Footer::pagination()acceptsClosure|array|string; all three shipped themes use the string alias. - CSS classes live in
->layout(Closure)forheader/table/footer;tabssets its classes directly onComponents\Tabs. - Prefer tokens over new blades. DaisyUI ships zero blades and inherits Tailwind's markup through
parentTheme+ the view-resolution fallback inTheme::doResolveView(). Add a Blade file only for genuinely different HTML (as Flux does). - The filter drawer is styled, not rebuilt.
filter.flyout.*(set with->flyout(fn (Components\Flyout $f) => ...)) drives the drawer used whenconfig('livewire-powergrid.filter')isflyout. Its blade is shared by every theme throughparentTheme, so override the classes and leaveviewalone.panelmust carry the positioning (fixed inset-y-0) and stay aboveoverlay;panel_left/panel_rightadd only the edge anchoring. Thedropdownvariant (config('...filter')=dropdown) works the same way. tabsis a theme-aware token group. Thetabs()section setslist/tab/tabActive/tabInactive/badge/badgeActive/badgeInactiveand (optionally)view. The shared base bladeresources/views/components/themes/tailwind/tabs.blade.phpreads these tokens and renders tab icons viaIconRenderer. Pointtabs.viewat your own blade only when the markup differs (Flux points it atpowergrid-plugins::Tabs.themes.flux).toggleable()fills color tokens, not a view. It setscolorOn/colorOff/colorOnDark/colorOffDark/knobOn; the shipped Toggleable blade reads them viatheme('toggleable.color_on')etc. Do not give it a->view().editable()is classes only. Do not seteditable.view. The plugin blade ispowergrid-plugins::Editable.index; keepclickable/input/error.filter.multi_selecthas no view token. Markup is<x-livewire-powergrid::inputs.select>; keepbase/selectclasses.- Feature views are auto-resolved. export, toggle-columns, soft-deletes, etc. resolve via
baseView + alias(then theparentThemechain, thencomponents.structure.*). Do not declare them. - View aliases: a value without
::is prefixed withbaseView + '.'. Use a fully-qualifiedlivewire-powergrid::...path only to point at another theme's view.
Completion checklist
-
struct()returnsThemeBuilder::make(...)->baseView(...)only (or an array forArrayTheme) -
protected ?string $parentTheme = Tailwind::class;is declared -
baseView()points at a real folder underresources/views/components/themes/ - Only the sections that differ from Tailwind are overridden, each as its own method (
layout/header/table/footer/cols/tabs/filter/editable/toggleable) - Sub-builder closures use the correct classes (
SearchBox/Checkbox/Radio/Body/Tr/Cols/Tabs), neverComponents\Component -
->pagination('pagination')(string form) -
editable()has no->view();toggleable()is colorfill()only;filter.multi_selecthas no view token - A Blade file is added only where the HTML genuinely differs; otherwise the parent's view is inherited
- Theme registered/selected in config by name or FQCN (new default only)
-
composer testpasses
See REFERENCE.md for the full section-method token map, the filter()/editable()/toggleable()/tabs() shapes, the complete builder API, dot-notation token keys, config theme_overrides, the theme registry, and the token/view resolution order.