teepluss/theme
Laravel theme management package for organizing Blade views, layouts, partials, and assets into switchable themes. Activate themes at runtime, resolve theme-specific templates, and keep multi-theme applications modular without duplicating view logic.
Install and register
composer require teepluss/theme
php artisan vendor:publish --provider="Teepluss\Theme\ThemeServiceProvider"
For Laravel 5.5+ the provider (Teepluss\Theme\ThemeServiceProvider) and facade (Teepluss\Theme\Facades\Theme) are auto-discovered; on older versions add them to config/app.php. Publish config lands in config/theme.php — the key setting is path, the base directory for all themes (typically public/themes).
Create your first theme
public/themes/default/
├── views/
│ ├── layouts/default.blade.php
│ ├── partials/header.blade.php
│ └── home.blade.php
└── assets/
├── css/style.css
└── js/app.js
First use case — replace a single view() call:
Theme::set('default');
return Theme::view('home', ['user' => $user]);
Theme::set() picks the active theme; Theme::view() resolves the view inside that theme's views/ directory, not the app's. Start with one controller or one route group, verify layouts and assets render, then expand.
Look here first: config/theme.php for the base path and default theme, the theme's views/layouts/ directory for the layout contract, and the service provider if you need to see how the view finder is registered.
Route-group theming (the core pattern)
Route::group(['middleware' => 'web'], function () {
Theme::set('admin');
Route::get('/admin', 'AdminController@index');
});
Wrap the block in middleware or a service provider so the theme is set before the route closure executes, not inside it.
Per-request theme selection via middleware
public function handle($request, Closure $next)
{
Theme::set($request->user()?->theme ?? config('theme.default'));
return $next($request);
}
This is the cleanest way to do per-tenant, per-domain, or per-user theming — one middleware, every controller uses Theme::view() unchanged.
Layouts and partials
Put the shell in views/layouts/default.blade.php and compose with partials:
{!! Theme::partial('header', ['title' => 'Dashboard']) !!}
@yield('content')
{!! Theme::partial('footer') !!}
Partials resolve relative to the theme, so Theme::partial('header') loads views/partials/header.blade.php.
Asset containers
Assets are registered into containers and flushed into the layout in the order you define:
Theme::asset()->add('assets/css/style.css', 'style');
Theme::asset()->container('footer')->add('assets/js/app.js', 'app');
{!! Theme::asset()->styles() !!}
{!! Theme::asset()->container('footer')->scripts() !!}
Register named assets once (a view composer or theme bootstrap) and let layouts emit them — this keeps controllers free of asset plumbing.
Theme-local configuration
A config.php inside the theme directory can declare the default layout and asset bundles, so a theme is self-contained and portable. This is the extension point for distributing a theme as a folder drop.
Coexistence with normal views
Controllers that don't need theming keep using view(). Mixing is fine as long as a single render path for a given URL consistently uses Theme::view() — a half-themed page usually means the action used view() while the layout expected theme partials.
This is a legacy package — check Laravel compatibility before committing. The composer constraint targets Laravel 5.x. On Laravel 9/10+ expect to fork, loosen the constraint, and re-verify the view finder registration and facade. For a new greenfield project, treat this as a stopgap and evaluate maintained alternatives; for an existing 5.x/6.x app, it's still solid.
Themes live under public/. Theme views and assets are web-accessible, which is why assets can be linked by relative URL. If you move the theme path outside public/, asset URLs break unless you rewire them through asset().
Theme::view() returns a View instance, not a string. return Theme::view(...) — never echo or string-concatenate it.
Don't prefix view names with the theme. Theme::view('default.home') fails; it's Theme::view('home'). The active theme is implicit.
Use {!! !!} for styles(), scripts(), and partial(). These return HTML; Blade's escaped syntax will print markup as text.
Stale output after editing a layout or partial is almost always the compiled-view cache: php artisan view:clear. If the default theme itself seems stuck on the wrong value, check config:cache — the published config is baked in.
Silent fallback vs. exception. A mistyped theme name usually surfaces as a view-not-found or a fallback to the configured default rather than a clear "theme missing" error. Log Theme::get() in the middleware when debugging.
Asset ordering is insertion order. If a vendor script must precede app.js, add it first or use separate containers (header / footer). Missing scripts() output in the layout produces no error — the asset just silently never renders.
Tests need explicit setup. Nothing sets a theme automatically inside a test, so call Theme::set('default') in setUp() or your rendered views resolve outside the theme tree.
Extension points worth knowing: the service provider (swapping the view finder), theme config.php (per-theme defaults and bundles), and view composers (registering assets without touching controllers). These three cover most customization without forking.
How can I help you explore Laravel packages today?