karlomikus/theme
A Laravel package for adding multi-theme support to your application. Manage theme views, assets, translations, and configuration with fallback handling, switch themes via middleware or helpers, and build customizable, white-label interfaces with minimal setup.
Install (package targets Laravel 5.x — verify constraints before adding to newer apps):
composer require karlomikus/theme
Register the provider — auto-discovered on 5.5+, otherwise add to config/app.php:
Karlomikus\Theme\ThemeServiceProvider::class,
'Theme' => Karlomikus\Theme\Facades\Theme::class,
Publish the config so you control the themes path:
php artisan vendor:publish --provider="Karlomikus\Theme\ThemeServiceProvider"
You get config/theme.php with themes_path, default_theme, and asset settings. Look here first — almost every quirk in this package is a path or a config-cache issue.
Create your first theme at the configured path:
themes/
Default/
theme.json # name, description, author, version
views/ # Blade templates
assets/ # css, js, images
theme.json is what the loader reads to discover the theme; without it the theme is invisible.
First use case — set the active theme and render a view:
Theme::set('Default');
return Theme::view('welcome', ['user' => $user]);
Theme::view() resolves against the active theme's views/ directory, so you stop hardcoding paths in controllers.
Per-request theme switching via middleware. This is the pattern most teams settle on — resolve the theme from a user preference, subdomain, or session, then set it before the controller runs:
public function handle($request, Closure $next)
{
Theme::set($request->user()->theme ?? config('theme.default_theme'));
return $next($request);
}
Keep it early in the web stack so every view render downstream inherits it.
Admin preview / multi-tenant layouts. Render any theme by name without changing the request-global default by setting, rendering, and resetting:
$previous = Theme::get();
Theme::set($request->input('theme'));
$html = Theme::view('home', $data)->render();
Theme::set($previous);
Useful for admin "preview theme" screens and for tenant-specific rendering inside a single queue job.
Asset URL helper in Blade. Do not hardcode asset('css/app.css') inside themes — use the package's asset helper so the URL follows the active theme:
<link rel="stylesheet" href="{{ theme_asset('css/app.css') }}">
Back it with a published/symlinked public/themes directory (or a route fallback) so the files resolve.
Layout inheritance across themes. Define a shared layouts/app.blade.php inside each theme at the same relative path. Controllers call Theme::view('pages.dashboard'); each theme owns its own layout, and swapping themes never touches controllers.
Theme list for pickers. Populate a settings dropdown from the package's theme enumeration (Theme::all() / the loader), not from File::directories() — the package already filters out folders without a valid theme.json.
Scaffold a new theme as a composer package. The cleanest integration in a larger codebase: put each theme in packages/themes/Acme with a small service provider that publishes views/ and assets/, then point themes_path at the published location. You get versioning and CI for free.
Testing. Set the theme in setUp() on the test case, not in the test body, so every Theme::view() assertion resolves deterministically. Use View::getFinder()->getPaths() in assertions if you need to prove the right theme was bound.
Laravel version ceiling. Last release was April 2019; the package targets Laravel 5.x. On Laravel 6+ expect to fork or pin with composer require karlomikus/theme --ignore-platform-reqs and then verify the service provider still binds correctly. Treat it as a vendored fork rather than an actively maintained dependency.
Config cache breaks theme discovery. php artisan config:cache freezes themes_path; if you deploy themes to a different directory per environment, clear the cache after deploy or the loader reads the stale path.
theme.json is mandatory and case-sensitive. A theme folder with no manifest (or a manifest with a parse error) silently drops out of Theme::all() and Theme::set() will fall back to the default. If a theme "doesn't exist," check the JSON first.
View cache persists across theme switches. Blade caches compiled views by name; if two themes have a welcome.blade.php at the same relative path, you can get the wrong compiled output after switching. The package namespaces views, but during development run php artisan view:clear whenever you swap a theme. In production this is a real bug class — test theme switching with the view cache warm.
Assets are not symlinked for you. theme_asset() builds a URL; it does not publish files. Add a deploy step (ln -s or php artisan vendor:publish) or your themes 404 on CSS.
Path traversal on user-supplied theme names. Theme::set($request->input('theme')) without validation is an arbitrary directory read. Always whitelist against Theme::all() before setting.
Default theme fallback is silent. When the active theme is missing, the package quietly uses default_theme instead of throwing. Wrap critical renders in a check (in_array($name, Theme::all())) if you need loud failures.
Extension point is thin. There is no driver/repository abstraction to swap — the loader reads from disk. If you need themes stored in a database or S3, you are writing a parallel implementation, not extending this package. Factor that into the build-vs-fork decision early.
Debug tip. Dump config('theme.themes_path'), Theme::all(), and Theme::get() together at the top of a failing view — 90% of "theme not found" issues are a wrong themes_path (often a missing base_path() in a queued job or console command) combined with a stale config cache.
How can I help you explore Laravel packages today?