5.4 KiB
Theme System
How the look of an installation is chosen and applied. Setting up a platform's theme and branding step by step: MULTI_PLATFORM_INSTANCES.md, sections 3, 5 and 6.
Last checked against the code: 2026-10-19 (branch laravel-11).
Where the active theme comes from
platform/theme.phpin the installation's platform folder: one theme definition with its ownid. When it exists,App\Providers\PlatformServiceProviderregisters it inthemes.themes.<id>and makes it the active theme.- Otherwise
config('themes.active'), which isTIMEBANK_THEMEin.env(defaulttimebank_cc), one of the themes inconfig/themes.php:timebank_cc,uuro,vegetable,yellow(templateconfig/themes.php.example, from whichdeploy.shcreates the file andconfig:mergeadds new keys).
A theme definition has name, description, logos, colors and typography. colors needs every key the
templates have (primary, secondary, button-secondary, button-secondary-text, accent, success, warning,
danger-dark, danger, danger-light, info, brand, background, border, surface, reaction,
reaction-hover, text, logo): a missing key leaves its CSS variable empty. Copy a whole block from
config/themes.php.example when you make a theme.
How it is applied: at runtime, one build for all themes
theme_css_vars()turns the active theme's colours and typography into CSS custom properties (--color-primary-500: 37 99 235;,--font_family_body: ...). The layouts (layouts/app,layouts/guest) put them in a<style>block on every page.tailwind.config.jsdefines the theme colours asrgb(var(--color-...) / <alpha-value>), so classes such asbg-theme-brand,text-theme-primary,border-theme-borderandbg-primary-500follow the active theme.- So switching themes needs no
npm run build: one asset build serves every theme. OnlyVITE_*values in.envare compiled into the build. <html data-theme="@themeId">carries the theme id; no CSS depends on it.
Logos, favicons and the preview image
Each asset is looked up in the platform folder first, then through the theme's logos settings:
| Asset | Platform folder | Theme setting | Helper |
|---|---|---|---|
| Website logo (inline SVG) | platform/views/logos/<theme>.blade.php |
logos.svg_inline, a view name such as logos.uuro (resources/views/logos/) |
theme_logo('svg_inline') |
| Email and PDF logo | platform/public/mail-logo.png |
logos.email_logo, relative to storage/app/public/ |
theme_email_logo_url() (emails), theme_email_logo_file() (PDFs, inline images) |
| Email logo for dark headers | platform/public/mail-logo-inverted.png |
logos.email_logo_inverted |
the same with true |
| Favicons | platform/public/ |
logos.favicon_dir, relative to public/ |
theme_favicon('favicon.ico'); theme_favicon_optional() for favicon.svg, favicon-96x96.png, site.webmanifest |
| Preview image for shared links | platform/public/og-image.png |
logos.og_image, relative to public/ |
theme_og_image() |
Without any of these, an email gets a generated PNG with the platform name (App\Services\GeneratedLogo; the
timebank_cc theme falls back to the tracked app_mail_logo.png), favicons come from public/, and there is no
preview image. The title, the icons and the Open Graph tags are in one partial that both layouts include:
resources/views/layouts/partials/brand-head.blade.php.
Helpers and Blade directives
PHP (app/Helpers/ThemeHelper.php) |
Blade (app/Providers/ThemeServiceProvider.php) |
Returns |
|---|---|---|
theme($key = null, $default = null) |
@theme('key') |
the active theme's definition, or one key of it |
theme_id() |
@themeId |
the active theme's id |
theme_name() |
@themeName |
its display name |
theme_color('primary.500') |
@themeColor('primary.500') |
a colour |
theme_font('font_family_body') |
@themeFont('font_family_body') |
a typography value |
is_theme('uuro') |
@isTheme('uuro') ... @endIsTheme |
whether that theme is active |
theme_css_vars() |
@themeCssVars |
the CSS custom properties |
theme_logo($type), theme_email_logo($inverted) and the helpers in the table above |
logos, favicons, preview image |
Prefer theme classes and helpers over theme checks: code that asks is_theme('uuro') breaks for a theme of a new
platform. Only the generated email logo checks the id (timebank_cc keeps its own PNG).
In views always use theme classes, never fixed colours. UI patterns: STYLE_GUIDE.md, THEME_CLASS_CONVERSION_GUIDE.md.
Adding a theme
- For one platform: in its
platform/theme.php, with its logo inplatform/views/logos/<id>.blade.php. Nothing in the code changes.php artisan platform:init --from-currentwrites the active theme there as a start. - For every installation: a block in
config/themes.php.exampleand a logo view inresources/views/logos/, committed to git.config:merge themesthen adds it to each installation'sconfig/themes.php.
Testing
tests/Feature/ThemeButtonColorsTest.php,PlatformBrandingTest.php,PlatformFolderTest.php(theme from the platform folder) andPlatformHeadTest.php(icons and Open Graph).- By hand: switch
TIMEBANK_THEMEin.envand reload; with aplatform/theme.phppresent, that one wins.