Theming
All components read CSS variables defined in
resources/css/ui.css.
Re-theming never requires editing component markup — you change tokens only.
Light and dark themes, plus admin and customer surfaces, are all expressed as token layers that
cascade via standard CSS specificity.
Semantic color tokens
Every swatch resolves to a CSS variable. Editing resources/css/ui.css re-themes the whole system.
How re-theming works
Existing themes store bare HSL channels ("H S% L%").
Each semantic mapping also accepts a full-color override such as
--primary-color: oklch(...), so teams can migrate to
perceptually uniform OKLCH without breaking an existing HSL theme. Tailwind opacity modifiers such as
bg-primary/90 continue to use the same semantic utility.
To swap your brand color, open
resources/css/ui.css
and change the --primary channel values in both
:root (light) and .dark:
/* resources/css/ui.css — change --primary for light + dark */
:root {
/* Default: inverse primary — the text colour is the fill */
--primary: 225 9% 17%;
--primary-foreground: 0 0% 100%;
/* Example: swap to indigo */
/* --primary: 243 75% 59%; */
/* --primary-foreground: 210 40% 98%; */
}
.dark {
/* Default dark: inverse again — near-white fill, near-black text */
--primary: 180 7% 97%;
--primary-foreground: 210 11% 4%;
/* Example: swap to indigo (lighter for dark bg) */
/* --primary: 243 75% 75%; */
/* --primary-foreground: 222.2 47.4% 11.2%; */
}
Note
H S% L%) — the
hsl() wrapper is already provided by the
@theme inline block. Never write
hsl(...) directly into a token value; the system
will double-wrap it.Token layers
ui.css is structured as five explicit layers that cascade in order:
Tailwind variants
dark,
surface-admin, and
surface-customer.
These are the CSS hooks that allow Tailwind utilities to respond to the active theme and surface.
@@theme inline — utility mapping
@theme inline.
The inline keyword is required: it inlines
hsl(var(--primary)) into each utility so the variable
resolves at the element — not once at :root.
This is what makes descendant .dark /
[data-surface] overrides work correctly.
This layer also maps semantic component metrics, radii, motion durations, and z-index layers.
@@theme — runtime scales
:root — light theme
--radius (default
1rem, the surface tier),
motion durations (--duration-fast/base/slow),
and the z-index scale.
.dark — dark theme overrides
.dark class is present on any ancestor element
(Tailwind's class-based dark mode strategy). Each rule has a copy for
data-theme="system" under
prefers-color-scheme: dark, for pages without a script
(dark mode without a script).
[data-surface] — surface overrides
[data-surface="admin"] and
[data-surface="customer"]
override product-context tokens on any subtree. Both scopes ship the same default accent and share
the one --radius; they exist so a brand
can split its admin and customer palettes without touching markup. Light and dark mode remain controlled by
.dark, not by the surface.
Add data-surface="admin" to your layout wrapper to activate it.
Token reference
The tokens below are the real custom properties defined in
ui.css.
Tailwind utilities are derived automatically: e.g.
--primary →
bg-primary /
text-primary /
border-primary.
Color tokens
| Token | Pair | Purpose |
|---|---|---|
| --background | --foreground | Page background and body text |
| --card | --card-foreground | Card surfaces and their text |
| --popover | --popover-foreground | Dropdown, tooltip and popover surfaces |
| --primary | --primary-foreground | Brand accent — buttons, active states, focus rings |
| --secondary | --secondary-foreground | Secondary actions and subtle fills |
| --muted | --muted-foreground | De-emphasized backgrounds and helper text |
| --accent | --accent-foreground | Hover fills on ghost/icon buttons |
| --destructive | --destructive-foreground | Errors, delete actions, danger badges |
| --success | --success-foreground | Confirmations, success badges |
| --warning | --warning-foreground | Warnings and advisory notices |
| --info | --info-foreground | Informational alerts and callouts |
| --border | --input | Dividers and form field borders |
| --ring | — | Keyboard focus ring color |
Radius scale
| Token | Value | Utility |
|---|---|---|
| --radius | 1rem (base, 16px) | Edit this single var to shift all rounded-* utilities |
| --radius-xs | calc(var(--radius) / 4) — 4px | rounded-xs — hairline tier (checkbox, swatch) |
| --radius-sm | calc(var(--radius) / 2) — 8px | rounded-sm — chip tier (menu item, kbd) |
| --radius-md | calc(var(--radius) * 3 / 4) — 12px | rounded-md — control tier (button, input, popover) |
| --radius-lg | var(--radius) — 16px | rounded-lg — surface tier (card, dialog, sheet) |
| --radius-xl / -2xl / -3xl | × 3/2, × 2, × 3 — 24 / 32 / 48px | rounded-xl, rounded-2xl, rounded-3xl |
Every tier is a ratio of --radius, so changing it in :root retunes the whole shape
while every step stays on the 4px half-grid. The surface tier pairs with the 24px --inset padding (p-6).
Shadow scale
| Utility | Description |
|---|---|
| shadow-xs | Subtle lift — inputs, inline chips |
| shadow-sm | Card resting state |
| shadow-md | Dropdowns, popovers |
| shadow-lg | Modals, dialogs |
Every layer is cast from hsl(var(--shadow-tint) / opacity) — a near-black channel triple in both modes, never the foreground, so dark mode gets depth instead of a white haze.
Motion
| Token | Value |
|---|---|
| --duration-fast | 120ms |
| --duration-base | 180ms |
| --duration-slow | 240ms |
| --ease-standard | cubic-bezier(0.2, 0, 0, 1) |
| --ease-emphasized | cubic-bezier(0.3, 0, 0, 1) |
Z-index scale
| Token | Value |
|---|---|
| --z-dropdown | 1000 |
| --z-sticky | 1100 |
| --z-overlay | 1200 |
| --z-modal | 1300 |
| --z-popover | 1400 |
| --z-toast | 1500 |
| --z-tooltip | 1600 |
Typography scale
Type sizes and weights are mapped in the runtime @theme block. The steps follow Tailwind's defaults
so existing utility classes work unchanged, with two grid-driven additions: an 11px text-2xs and a 64px text-6xl.
| Token | Size / Line-height |
|---|---|
| --text-2xs | 0.6875rem / 1rem — 11px (meta labels, instead of an arbitrary text-[11px]) |
| --text-xs | 0.75rem / 1rem — 12px |
| --text-sm | 0.875rem / 1.25rem — 14px |
| --text-base | 1rem / 1.5rem — 16px |
| --text-lg | 1.125rem / 1.75rem — 18px |
| --text-xl | 1.25rem / 1.75rem — 20px |
| --text-2xl | 1.5rem / 2rem — 24px |
| --text-3xl | 1.875rem / 2.25rem — 30px |
| --text-4xl | 2.25rem / 2.5rem — 36px |
| --text-5xl | 3rem / 1 — 48px |
| --text-6xl | 4rem / 1 — 64px (not Tailwind's 60px, so every display step lands on the 8px grid) |
| --text-7xl | 4.5rem / 1 — 72px |
| --text-8xl | 6rem / 1 — 96px |
| --text-9xl | 8rem / 1 — 128px |
Note
bg-blue-500 or an inline
hsl(...) style inside a component or block is forbidden and is
enforced by BlockRenderTest
(test_block_never_ships_raw_colour_utilities).
All color expression must go through the semantic token layer so that surfaces, dark mode,
and future theme changes are respected automatically.Dark mode without a script
The theme script (<x-ui.theme-script />)
sets .dark from the stored choice and needs
script-src 'self' or a nonce. A page that runs no script at all,
for example an OAuth sign-in or consent page served with
default-src 'none'; style-src 'self',
sets data-theme="system" on
<html> instead. The tokens in
ui.css and in every generated theme then follow the
operating system's colour scheme through prefers-color-scheme.
<!DOCTYPE html>
<html lang="en" data-theme="system">
<head>
<meta name="color-scheme" content="light dark">
@vite('resources/css/app.css')
</head>
<body class="bg-background text-foreground">…</body>
</html>
Note
dark: utilities still need .dark.
To keep the reader's stored choice on a strict-CSP page instead, allow the theme script with a nonce:
<x-ui.theme-script :nonce="$nonce" /> and
script-src 'nonce-…'.Using surfaces
Surfaces let you scope a completely different token set to a subtree without touching the global theme.
Add the data-surface attribute to any wrapper element.
{{-- Admin surface: override --primary / --ring here for a separate admin palette --}}
<div data-surface="admin">
<brok:sidebar />
<brok:data-table />
</div>
{{-- Customer surface: same default accent; retune it independently of admin --}}
<div data-surface="customer">
<brok:hero />
<brok:pricing-card />
</div>
Surface overrides apply to the entire subtree — all descendant components automatically pick up the
correct --primary,
--radius, and other tokens
without any changes to component markup. Admin blocks must pin to the admin surface;
this is enforced by the block test suite.
Density and component metrics
Control heights, horizontal padding, field gaps, card padding, dialog padding, and section rhythm are semantic tokens.
Set data-density="compact" or data-density="comfortable"
on any subtree to change its density without editing individual components.
<main data-density="compact">
<brok:button>Compact action</brok:button>
<brok:input placeholder="Compact input" />
</main>
<section data-density="comfortable">
<brok:button>Comfortable action</brok:button>
</section>
The five physical control tiers sit on the 8px grid: 24, 32, 40, 48, and 56px. Component APIs expose
semantic sm / md / lg sizes; the normal
md control resolves to the 40px tier.
Density remaps those semantic sizes to the fixed tiers instead of inventing new heights.
The spacing tokens feed the layout primitives (stack, grid,
section, …) — see the
layout grammar.
OKLCH overrides
Prefer OKLCH for new brand palettes. Keep values inside the sRGB gamut unless you also provide a wider-gamut enhancement, and verify foreground/background pairs in both light and dark modes.
:root {
/* Full-color values override the legacy HSL channel fallback. */
--primary-color: oklch(0.637 0.237 25.331);
--primary-foreground-color: oklch(0.985 0 0);
}
@media (color-gamut: p3) {
:root {
/* Optional wider-gamut enhancement after testing clipping. */
--primary-color: oklch(0.637 0.26 25.331);
}
}
Custom theme example
To build a green-primary, fully rounded theme, override only the tokens you need. Every other value inherits from the defaults.
/* resources/css/ui.css — custom brand theme */
:root {
/* Brand: forest green */
--primary: 142 71% 45%;
--primary-foreground: 0 0% 100%;
/* Rounder corners (default is 1rem) */
--radius: 1.5rem;
/* Warmer muted tones */
--muted: 120 10% 94%;
--muted-foreground: 120 6% 45%;
}
.dark {
--primary: 142 65% 55%;
--primary-foreground: 142 10% 10%;
}
Note
:root defaults. The CSS variables remain the editable theme source.
Curated presets can also be installed as editable registry
layers with php artisan ui:add themes/zinc; custom Theme
Generator JSON exports install with php artisan ui:theme ./theme.json --name=my-brand.