Skip to content
Brok UI

Loading…

No results

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

primary
secondary
muted
destructive
success
warning

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 CSS
/* 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

Only change the channel values (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

Three custom variants are registered: 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

Maps every token onto a Tailwind utility via @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

Defines runtime-overridable spacing, typography, content widths, shadows, and easing. These values stay as custom-property references so the theme studio can change them without a rebuild.

:root — light theme

All primitive token values for the light theme, including color channels, the base --radius (default 1rem, the surface tier), motion durations (--duration-fast/base/slow), and the z-index scale.

.dark — dark theme overrides

Overrides the color channels for dark mode. Applied when the .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

Components must use semantic tokens — never raw colours. Using a raw Tailwind color utility such as 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.

resources/views/oauth/consent.blade.php Blade
<!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

Use the attribute only on pages without the theme script: it has no stored choice, so a reader who picked light on another page still sees dark here when the system is dark. Brok components read the tokens, so they follow it; your own 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.

resources/views/layouts/app.blade.php Blade
{{-- 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.

resources/views/layouts/app.blade.php Blade
<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.

resources/css/ui.css CSS
: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 CSS
/* 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

You only need to declare the tokens you want to change. Unset tokens fall through to the :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.