alizharb/laravel-themer
Enterprise-grade theme management for Laravel. Create, clone, activate, and safely delete themes with per-theme Vite builds, NPM workspaces, asset shortcuts, view overrides, and Livewire 4 support. Includes metadata, wizards, and fast production caching.
Laravel Themer provides first-class support for Livewire 4, with automatic component discovery, theme-specific namespaces, and deep inheritance.
php artisan make:livewire home --class --theme=mytheme
Creates:
themes/mytheme/app/Livewire/Home.phpthemes/mytheme/resources/views/livewire/home.blade.phpthemes/mytheme/app/Livewire/Home.php:
<?php
namespace Theme\MyTheme\Livewire;
use Livewire\Component;
class Home extends Component
{
public string $title = 'Welcome';
public function render()
{
return view('livewire.home');
}
}
themes/mytheme/resources/views/livewire/pages/home.blade.php:
<div>
<h1>{{ $title }}</h1>
<p>This is a theme-specific Livewire component.</p>
</div>
Laravel Themer automatically registers theme-specific Livewire namespaces.
Each theme gets its own namespace based on the theme slug:
{{-- Renders component from active theme --}}
<livewire:mytheme::home />
Configured in config/themer.php:
'auto_namespaces' => [
'layouts' => 'resources/views/layouts',
'pages' => 'resources/views/livewire/pages',
],
Usage:
{{-- Resolves to active theme's pages directory --}}
<livewire:pages::home />
{{-- Resolves to active theme's layouts directory --}}
<livewire:layouts::navigation />
When a theme is activated, Laravel Themer automatically:
app/Livewire/ for class-based componentsresources/views/livewire/ for view-based components (SFC/MFC)Create single-file or multi-file components directly in views:
themes/mytheme/resources/views/livewire/
├── pages/
│ ├── ⚡home.blade.php # Single-file component
│ └── about/
│ ├── ⚡about.blade.php # Multi-file component
│ └── ⚡about.js
Usage:
<livewire:pages::home />
<livewire:pages::about />
Laravel Themer supports infinite inheritance depth with automatic loop protection.
Example Hierarchy:
base-theme/
└── app/Livewire/Components/Button.php
corporate-theme/
└── app/Livewire/Components/Button.php ← Overrides
corporate-dark/
└── app/Livewire/Components/Button.php ← Overrides again
When rendering <livewire:button />:
corporate-dark)corporate-theme)base-theme)app/Livewire)If a component doesn't exist in the active theme, Laravel Themer automatically searches parent themes:
{{-- Component exists in parent theme --}}
<livewire:components::card />
php artisan livewire:layout --theme=mytheme
themes/mytheme/resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? config('app.name') }}</title>
[@vite](https://github.com/vite)(['resources/assets/css/app.css', 'resources/assets/js/app.js'], 'themes/mytheme')
</head>
<body>
{{ $slot }}
</body>
</html>
public function render()
{
return view('livewire.home')
->layout('layouts::app');
}
Or configure globally in config/livewire.php:
'component_layout' => 'layouts::app',
Route::get('/', function () {
return view('livewire.pages.home');
})->middleware('web');
// Or using Livewire routing
Route::get('/about', \Theme\MyTheme\Livewire\About::class);
<?php
namespace Theme\MyTheme\Livewire\Pages;
use Livewire\Component;
class About extends Component
{
public function render()
{
return view('livewire.about')
->layout('layouts::app')
->title('About Us');
}
}
<div>
<h1>Dashboard</h1>
<livewire:components::stats-card
:title="'Total Users'"
:value="$userCount"
/>
<livewire:components::stats-card
:title="'Revenue'"
:value="$revenue"
/>
</div>
<?php
namespace Theme\MyTheme\Livewire\Components;
use Livewire\Component;
class StatsCard extends Component
{
public string $title;
public mixed $value;
public function render()
{
return view('livewire.components.stats-card');
}
}
<div>
<input type="text" wire:model.live="search" placeholder="Search...">
[@foreach](https://github.com/foreach)($results as $result)
<div>{{ $result->name }}</div>
[@endforeach](https://github.com/endforeach)
</div>
public string $search = '';
public function getResultsProperty()
{
return User::where('name', 'like', "%{$this->search}%")->get();
}
public function save()
{
// Save logic...
$this->dispatch('user-saved', userId: $this->user->id);
}
protected $listeners = ['user-saved' => 'refreshData'];
public function refreshData($userId)
{
// Refresh component data
}
$this->dispatch('show-toast', message: 'Saved successfully!');
<div x-data [@show-toast](https://github.com/show-toast).window="alert($event.detail.message)">
<!-- Component content -->
</div>
<?php
namespace Theme\MyTheme\Livewire\Forms;
use Livewire\Component;
use Livewire\Attributes\Validate;
class ContactForm extends Component
{
#[Validate('required|min:3')]
public string $name = '';
#[Validate('required|email')]
public string $email = '';
#[Validate('required|min:10')]
public string $message = '';
public function submit()
{
$this->validate();
// Send email logic...
session()->flash('success', 'Message sent!');
$this->reset();
}
public function render()
{
return view('livewire.forms.contact-form');
}
}
<form wire:submit="submit">
<div>
<label>Name</label>
<input type="text" wire:model="name">
[@error](https://github.com/error)('name') <span>{{ $message }}</span> [@enderror](https://github.com/enderror)
</div>
<div>
<label>Email</label>
<input type="email" wire:model="email">
[@error](https://github.com/error)('email') <span>{{ $message }}</span> [@enderror](https://github.com/enderror)
</div>
<div>
<label>Message</label>
<textarea wire:model="message"></textarea>
[@error](https://github.com/error)('message') <span>{{ $message }}</span> [@enderror](https://github.com/enderror)
</div>
<button type="submit">Send Message</button>
</form>
use Livewire\WithFileUploads;
class UploadForm extends Component
{
use WithFileUploads;
#[Validate('image|max:1024')]
public $photo;
public function save()
{
$this->validate();
$path = $this->photo->store('photos', 'public');
// Save to database...
}
}
<form wire:submit="save">
<input type="file" wire:model="photo">
[@if](https://github.com/if) ($photo)
<img src="{{ $photo->temporaryUrl() }}">
[@endif](https://github.com/endif)
<button type="submit">Upload</button>
</form>
✅ <livewire:pages::home />
❌ <livewire:home />
Design reusable base components in parent themes.
Each component should have a single responsibility.
<livewire:heavy-component lazy />
Use computed properties for expensive operations:
public function getUsersProperty()
{
return User::with('profile')->get();
}
How can I help you explore Laravel packages today?