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: dark navy */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Example: swap to indigo */
/* --primary: 243 75% 59%; */
/* --primary-foreground: 210 40% 98%; */
}
.dark {
/* Default dark: near-white */
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* 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 defines the radius, typography, shadow, and motion easing scales.
:root — light theme
--radius (default
0.5rem),
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).
[data-surface] — surface overrides
[data-surface="admin"] and
[data-surface="customer"]
override product-context tokens on any subtree. The admin surface has a denser, cooler aesthetic
with a blue primary and a tighter radius (0.375rem);
the customer surface keeps a warmer orange primary. 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 | 0.5rem (base) | Edit this single var to shift all rounded-* utilities |
| --radius-sm | calc(var(--radius) - 4px) | rounded-sm |
| --radius-md | calc(var(--radius) - 2px) | rounded-md |
| --radius-lg | var(--radius) | rounded-lg |
| --radius-xl | calc(var(--radius) + 4px) | rounded-xl |
Changing --radius in :root shifts the whole scale proportionally.
Surface overrides can pin a different base radius (e.g. admin uses 0.375rem).
Shadow scale
| Utility | Description |
|---|---|
| shadow-xs | Subtle lift — inputs, inline chips |
| shadow-sm | Card resting state |
| shadow-md | Dropdowns, popovers |
| shadow-lg | Modals, dialogs |
All shadow values use hsl(var(--foreground) / opacity) so they automatically invert between light and dark themes.
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 @theme inline. The step values match Tailwind's defaults
so existing utility classes work unchanged.
| Token | Size / Line-height |
|---|---|
| --text-xs | 0.75rem / 1rem |
| --text-sm | 0.875rem / 1.25rem |
| --text-base | 1rem / 1.5rem |
| --text-lg | 1.125rem / 1.75rem |
| --text-xl | 1.25rem / 1.75rem |
| --text-2xl | 1.5rem / 2rem |
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.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: blue primary, 0.375rem radius --}}
<div data-surface="admin">
<x-ui.sidebar />
<x-ui.data-table />
</div>
{{-- Customer surface: orange primary, 0.5rem radius --}}
<div data-surface="customer">
<x-ui.hero />
<x-ui.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">
<x-ui.button>Compact action</x-ui.button>
<x-ui.input placeholder="Compact input" />
</main>
<section data-density="comfortable">
<x-ui.button>Comfortable action</x-ui.button>
</section>
The five physical control tiers are 32, 36, 40, 44, and 48px. Component APIs expose
semantic sm / md / lg sizes; the normal
md control resolves to a 44px-friendly target.
Density remaps those semantic sizes to the fixed tiers instead of inventing new heights.
Layout grammar
Build screens from constrained layout primitives. Their props map to literal, discoverable classes and unknown values fall back safely, so feature code never needs arbitrary pixel gaps.
<x-ui.section space="lg" surface="muted">
<x-ui.container size="lg">
<x-ui.stack gap="lg">
<x-ui.prose :centered="false">
<h2>Workspace activity</h2>
<p>Readable copy wraps safely, including long translated strings and URLs.</p>
</x-ui.prose>
<x-ui.grid cols="3" gap="md">
<x-ui.surface>…</x-ui.surface>
<x-ui.surface>…</x-ui.surface>
<x-ui.surface>…</x-ui.surface>
</x-ui.grid>
<x-ui.cluster gap="sm" justify="end">
<x-ui.button variant="outline">Cancel</x-ui.button>
<x-ui.button>Save</x-ui.button>
</x-ui.cluster>
</x-ui.stack>
</x-ui.container>
</x-ui.section>
Use stack for flow, cluster
for wrapping inline groups, grid for columns,
container and section for page rhythm,
surface for panels, prose for readable text,
and bleed scroll="true" for intentionally wide tables or code.
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 */
--radius: 0.75rem;
/* 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.