Theming & skins
Tiger separates how the app is structured from how it looks — two axes, deliberately different weights.
| Theme | Skin | |
|---|---|---|
| Is | a whole view layer (layouts, view scripts) | a CSS-only override |
| Weight | heavy, changes rarely | light, swappable, structurally inert |
| Per-tenant? | rare (white-label) | yes — this is the tenant branding axis |
| Example | puma (a future react theme) |
jaguar, cheetah, <tenant>.css |
PUMA — the default theme, zero build
PUMA is vendored Bootstrap 5 — bootstrap.min.css + the bundle, dropped in. No npm, no Sass, no
build step. composer install + an asset symlink and the UI renders. (Node only ever appears if a
theme opts into a build — quarantined there.)
Skins are runtime CSS variables
A skin is a small :root { --bs-* } override (plus component --bs-btn-* vars). Because it's just
CSS variables, it swaps at runtime — no recompile. Skins resolve per-user (a cookie) and
per-org (a config row), and the admin ships a live skin switcher that hot-swaps them with no
reload. Light / dark rides Bootstrap's native data-bs-theme with a browser/light/dark toggle and
no-flash resolution.
Theme is just a path
The active theme + skin resolve from the config cascade at bootstrap; "active theme" is simply a
path woven into the layout path, the view-script paths, and the asset base URL. No inheritance, no
routing — the only fallback is theme → core default views, so a theme only provides what it wants to
override. Because per-org theming is the config cascade (a tenant's tiger.skin is an org-scoped
config row), the whole thing costs almost nothing.
Assets
Asset URLs are root-relative (/_theme/…, never a hardcoded domain) and cache-busted by
$this->asset() (?v=<filemtime>), so a deploy's changed CSS/JS is picked up without a manifest or a
hard refresh.
See also
- Configuration — the cascade that resolves the active theme/skin.
- Multi-tenancy — per-org branding is the same mechanism.