Themes Gallery
A theme showcase generated from the portable brand-token catalog. Each card shows primary, secondary, and accent colors and can re-skin a scoped live component preview in light or dark mode.
Preview
@props([
// Section copy (baked sample content — it's a block, not a primitive).
'eyebrow' => 'Theming',
'heading' => 'Pick a theme, see it live',
'subheading' => 'Eight curated presets. Click one to instantly re-skin the live preview — the same design tokens your whole app reads.',
])
{{--
Themes gallery. A grid of eight preset-theme cards drives a live component
preview rendered inside the block. Selecting a card binds that preset's token
channel values as inline custom properties on the preview wrapper
(:style="previewVars"), which cleanly scopes the theme to just the preview —
no <style> injection, no leakage. Values resolve for the current page mode
(light/dark) inside uiThemesGallery.
Every colour is bound at runtime from JS (dots via :style, preview via token
vars) — there are deliberately NO colour utilities or inline colour literals
in this markup.
--}}
<x-ui.section
x-data="uiThemesGallery()"
x-init="init()"
data-slot="themes-gallery"
space="none" {{ $attributes }}
>
<div class="mx-auto max-w-6xl px-4 py-16 sm:py-20">
<div class="mx-auto max-w-2xl text-center">
@if ($eyebrow)
<x-ui.badge variant="secondary" class="mb-4">{{ __($eyebrow) }}</x-ui.badge>
@endif
<h2 class="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
{{ __($heading) }}
</h2>
<p class="mx-auto mt-4 max-w-xl text-pretty text-base text-muted-foreground">
{{ __($subheading) }}
</p>
</div>
<div class="mt-12 grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,22rem)]">
{{-- Preset grid --}}
<div
role="group"
aria-label="{{ __('Preset themes') }}"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"
>
<template x-for="preset in presets" :key="preset.key">
<button
type="button"
@click="select(preset.key)"
:aria-pressed="isActive(preset.key) ? 'true' : 'false'"
:class="isActive(preset.key)
? 'border-transparent ring-2 ring-ring ring-offset-2 ring-offset-background'
: 'border-border hover:border-foreground/20'"
class="flex flex-col items-start gap-4 rounded-lg border bg-card p-6 text-start transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none"
>
<span class="flex items-center gap-2" aria-hidden="true">
<template x-for="(dot, i) in dots(preset)" :key="i">
<span
class="size-4 rounded-full border border-border/40 shadow-sm"
:style="`background-color: hsl(${dot})`"
></span>
</template>
</span>
<span class="text-sm font-medium text-foreground" x-text="preset.label"></span>
</button>
</template>
</div>
{{-- Live preview — themed by inline token vars scoped to this wrapper. --}}
<div
data-slot="themes-gallery-preview"
:style="previewVars"
aria-live="polite"
>
<x-ui.card class="overflow-hidden">
<x-ui.card.header>
<div class="flex items-center justify-between gap-4">
<x-ui.card.title>{{ __('Live preview') }}</x-ui.card.title>
<x-ui.badge x-text="activePreset.label">{{ __('Default') }}</x-ui.badge>
</div>
<p class="text-sm text-muted-foreground">
{{ __('Real primitives, re-skinned by the selected theme.') }}
</p>
</x-ui.card.header>
<x-ui.card.content class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-4">
<x-ui.button>{{ __('Primary') }}</x-ui.button>
<x-ui.button variant="secondary">{{ __('Secondary') }}</x-ui.button>
<x-ui.button variant="outline">{{ __('Outline') }}</x-ui.button>
</div>
<div class="flex flex-col gap-2">
<label for="themes-gallery-email" class="text-sm font-medium text-foreground">
{{ __('Email') }}
</label>
<x-ui.input
id="themes-gallery-email"
type="email"
name="email"
autocomplete="email"
placeholder="you@example.com"
/>
</div>
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge>{{ __('Default') }}</x-ui.badge>
<x-ui.badge variant="secondary">{{ __('Secondary') }}</x-ui.badge>
<x-ui.badge variant="outline">{{ __('Outline') }}</x-ui.badge>
</div>
</x-ui.card.content>
</x-ui.card>
</div>
</div>
</div>
</x-ui.section>
/**
* Themes gallery — a live preset-theme switcher.
*
* Renders the generated brand catalog as colour-dot cards. Selecting one
* re-skins a live component preview rendered inside the block by binding the
* preset's design-token CHANNEL values (the `H S% L%` triples our utilities feed
* to `var(--token-color, hsl(var(--token)))`) as inline CSS custom properties on the preview
* wrapper. Inline style on that single wrapper scopes the theme to just the
* preview — there is no `<style>` injection and no leakage to the rest of the
* page.
*
* Each preset carries a LIGHT and a DARK token map. The `active` getter resolves
* to whichever matches the current page mode (the `.dark` class on
* <html>), so the preview always reflects how the theme reads in the mode the
* visitor is actually viewing. We observe the document root's class list so a
* mode toggle elsewhere on the page re-resolves the preview without a reload.
*
* Self-registers on `alpine:init` so import order does not matter; the glob
* importer picks this file up automatically.
*
* Cleanup: the only side effect is a MutationObserver on <html>; destroy()
* disconnects it so nothing leaks when Alpine tears the node down.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiThemesGallery', () => ({
// Generated preset set. Each preset has a label plus `light` / `dark`
// token maps of bare HSL channel triples (consumed via var(--x-color, hsl(var(--x)))).
// `secondary` / `accent` are only used to paint the card preview dots;
// the live preview themes via primary / primaryForeground / ring /
// radius. `radius` is mode-independent.
// <design-tokens:presets>
// Generated by scripts/design-tokens.mjs from tokens/brands/.
presets: [
{
"key": "default",
"label": "Default",
"radius": "1.5rem",
"light": {
"primary": "222.2 47.4% 11.2%",
"primaryForeground": "210 40% 98%",
"ring": "222.2 84% 4.9%",
"secondary": "225 22% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "210 40% 98%",
"primaryForeground": "222.2 47.4% 11.2%",
"ring": "212.7 26.8% 83.9%",
"secondary": "210 4% 10%",
"accent": "220 4% 11%"
}
},
{
"key": "zinc",
"label": "Zinc",
"radius": "0.5rem",
"light": {
"primary": "240 6% 10%",
"primaryForeground": "0 0% 98%",
"ring": "240 6% 10%",
"secondary": "240 5% 96%",
"accent": "240 5% 93%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "240 6% 10%",
"ring": "240 5% 84%",
"secondary": "240 4% 14%",
"accent": "240 4% 17%"
}
},
{
"key": "slate",
"label": "Slate",
"radius": "0.75rem",
"light": {
"primary": "215 25% 27%",
"primaryForeground": "210 40% 98%",
"ring": "215 25% 27%",
"secondary": "214 32% 91%",
"accent": "210 40% 94%"
},
"dark": {
"primary": "210 40% 90%",
"primaryForeground": "222 47% 11%",
"ring": "210 40% 80%",
"secondary": "217 33% 17%",
"accent": "217 33% 20%"
}
},
{
"key": "rose",
"label": "Rose",
"radius": "1rem",
"light": {
"primary": "346.8 77.2% 48%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "346.8 77.2% 48%",
"secondary": "350 100% 96%",
"accent": "350 100% 94%"
},
"dark": {
"primary": "346.8 77.2% 48%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "346.8 77.2% 48%",
"secondary": "345 25% 15%",
"accent": "345 30% 18%"
}
},
{
"key": "emerald",
"label": "Emerald",
"radius": "1rem",
"light": {
"primary": "142.1 76.2% 28%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "142.1 76.2% 28%",
"secondary": "150 55% 94%",
"accent": "150 60% 92%"
},
"dark": {
"primary": "142.1 70.6% 45.3%",
"primaryForeground": "144.9 80.4% 10%",
"ring": "142.1 70.6% 45.3%",
"secondary": "150 25% 14%",
"accent": "150 28% 17%"
}
},
{
"key": "violet",
"label": "Violet",
"radius": "1.25rem",
"light": {
"primary": "262.1 83.3% 57.8%",
"primaryForeground": "210 20% 98%",
"ring": "262.1 83.3% 57.8%",
"secondary": "262 80% 96%",
"accent": "262 80% 94%"
},
"dark": {
"primary": "263.4 70% 50.4%",
"primaryForeground": "210 20% 98%",
"ring": "263.4 70% 55%",
"secondary": "262 30% 16%",
"accent": "262 32% 19%"
}
},
{
"key": "midnight",
"label": "Midnight",
"radius": "0.75rem",
"light": {
"primary": "224 76.3% 48%",
"primaryForeground": "210 40% 98%",
"ring": "224 76.3% 48%",
"secondary": "220 60% 95%",
"accent": "222 70% 93%"
},
"dark": {
"primary": "217.2 91.2% 59.8%",
"primaryForeground": "222.2 47.4% 11.2%",
"ring": "217.2 91.2% 59.8%",
"secondary": "220 35% 15%",
"accent": "220 40% 18%"
}
},
{
"key": "sunset",
"label": "Sunset",
"radius": "1rem",
"light": {
"primary": "24.6 95% 38%",
"primaryForeground": "60 9.1% 97.8%",
"ring": "24.6 95% 38%",
"secondary": "30 80% 93%",
"accent": "25 90% 90%"
},
"dark": {
"primary": "20.5 90.2% 48.2%",
"primaryForeground": "20 14.3% 4.1%",
"ring": "20.5 90.2% 48.2%",
"secondary": "20 25% 14%",
"accent": "20 30% 18%"
}
},
{
"key": "spotify",
"label": "Spotify",
"radius": "2rem",
"light": {
"primary": "141.6 73.4% 30%",
"primaryForeground": "0 0% 100%",
"ring": "141.6 73.4% 30%",
"secondary": "0 0% 95%",
"accent": "141 60% 92%"
},
"dark": {
"primary": "141.9 69.2% 58%",
"primaryForeground": "141 80% 8%",
"ring": "141.9 69.2% 58%",
"secondary": "0 0% 16%",
"accent": "141 30% 14%"
}
},
{
"key": "neo",
"label": "Neo",
"radius": "0rem",
"light": {
"primary": "0 0% 4%",
"primaryForeground": "0 0% 98%",
"ring": "0 0% 4%",
"secondary": "52 100% 60%",
"accent": "190 100% 85%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 4%",
"ring": "0 0% 98%",
"secondary": "52 100% 60%",
"accent": "190 80% 25%"
}
},
{
"key": "claude",
"label": "Claude",
"radius": "0.75rem",
"light": {
"primary": "15.2 63.1% 59.2%",
"primaryForeground": "20 50% 10%",
"ring": "15.2 63.1% 52%",
"secondary": "30 35% 91%",
"accent": "18 70% 90%"
},
"dark": {
"primary": "15.2 55.4% 52.5%",
"primaryForeground": "20 50% 10%",
"ring": "15.2 55.4% 52.5%",
"secondary": "20 15% 16%",
"accent": "18 30% 20%"
}
},
{
"key": "monogram",
"label": "Monogram",
"radius": "0rem",
"light": {
"primary": "0 0% 12.5%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 12.5%",
"secondary": "0 0% 95.7%",
"accent": "0 0% 95.7%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 12.5%",
"ring": "0 0% 83.9%",
"secondary": "0 0% 14.9%",
"accent": "0 0% 14.9%"
}
},
{
"key": "prestige",
"label": "Prestige",
"radius": "0rem",
"light": {
"primary": "0 0% 11%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 11%",
"secondary": "0 0% 88%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "0 0% 11%",
"ring": "0 0% 84%",
"secondary": "0 0% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "showcase",
"label": "Showcase",
"radius": "0rem",
"light": {
"primary": "240 9% 30%",
"primaryForeground": "0 0% 100%",
"ring": "240 9% 30%",
"secondary": "240 9% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "240 9% 92%",
"primaryForeground": "240 9% 12%",
"ring": "240 9% 84%",
"secondary": "240 8% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "madrid",
"label": "Madrid",
"radius": "0rem",
"light": {
"primary": "0 0% 0%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 0%",
"secondary": "0 0% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 0%",
"ring": "0 0% 84%",
"secondary": "0 0% 15%",
"accent": "220 4% 11%"
}
},
{
"key": "xclusive",
"label": "Xclusive",
"radius": "0rem",
"light": {
"primary": "0 0% 12%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 12%",
"secondary": "0 0% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "0 0% 12%",
"ring": "0 0% 84%",
"secondary": "0 0% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "alchemy",
"label": "Alchemy",
"radius": "0rem",
"light": {
"primary": "240 7% 11%",
"primaryForeground": "0 0% 100%",
"ring": "240 7% 11%",
"secondary": "240 6% 88%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "240 7% 11%",
"ring": "0 0% 84%",
"secondary": "240 7% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "reformation",
"label": "Reformation",
"radius": "0rem",
"light": {
"primary": "228 31% 23%",
"primaryForeground": "0 0% 100%",
"ring": "228 31% 23%",
"secondary": "228 30% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "228 30% 72%",
"primaryForeground": "228 30% 12%",
"ring": "228 30% 72%",
"secondary": "228 25% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "sampo",
"label": "Sampo",
"radius": "0rem",
"light": {
"primary": "30 8% 19%",
"primaryForeground": "0 0% 100%",
"ring": "30 8% 19%",
"secondary": "30 8% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "30 8% 82%",
"primaryForeground": "30 8% 12%",
"ring": "30 8% 82%",
"secondary": "30 8% 16%",
"accent": "220 4% 11%"
}
}
],
// </design-tokens:presets>
activeKey: 'default',
// Bumped on a mode change so the `active`/`previewVars` getters recompute.
mode: 'light',
_observer: null,
init() {
this.mode = this._currentMode();
// Re-resolve the preview if the page mode toggles elsewhere.
this._observer = new MutationObserver(() => {
const next = this._currentMode();
if (next !== this.mode) this.mode = next;
});
this._observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
},
destroy() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
},
_currentMode() {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
},
// The currently selected preset (whole object).
get activePreset() {
return this.presets.find((p) => p.key === this.activeKey) || this.presets[0];
},
// Token map for the current preset resolved to the current page mode,
// plus the mode-independent radius.
get active() {
const p = this.activePreset;
// `this.mode` is read so this getter is reactive to mode changes.
const tokens = this.mode === 'dark' ? p.dark : p.light;
return { ...tokens, radius: p.radius };
},
// CSS custom properties bound inline on the preview wrapper. Setting the
// channel vars directly works because the utilities resolve them through
// var(--token-color, hsl(var(--token))); --radius is the raw length.
get previewVars() {
const a = this.active;
return {
'--primary': a.primary,
'--primary-foreground': a.primaryForeground,
'--ring': a.ring,
'--radius': a.radius,
};
},
// The three dots shown on a preset card, resolved for the current mode.
dots(preset) {
const tokens = this.mode === 'dark' ? preset.dark : preset.light;
return [tokens.primary, tokens.secondary, tokens.accent];
},
isActive(key) {
return this.activeKey === key;
},
select(key) {
this.activeKey = key;
},
}));
});
Installation
php artisan ui:add blocks/themes-gallery
Registry contract
Install confidence
php artisan ui:add blocks/themes-gallery
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.1.5
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/themes-gallery.blade.php |
| js | resources/js/ui/themes-gallery.js |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Themes Gallery (`themes-gallery`)
A theme showcase generated from the portable brand-token catalog. Each card shows primary, secondary, and accent colors and can re-skin a scoped live component preview in light or dark mode.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/themes-gallery
```
## Render it
```blade
<x-blocks.themes-gallery />
```
## Props
- `eyebrow` (string, default `Theming`) — Small label shown above the heading.
- `heading` (string, default `Pick a theme, see it live`) — Section heading.
- `subheading` (string, default `Eight curated presets. Click one to instantly re-skin the live preview — the same design tokens your whole app reads.`) — Supporting copy under the heading.
## Use when
- Showcasing preset brand themes as cards that re-skin a live component preview when clicked.
- Letting visitors try different themes interactively on a docs or marketing page.
## Avoid when
- Documenting individual token values rather than whole-theme presets; use style-guide instead.
## Rules
- Render the installed block with `<x-blocks.themes-gallery />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/themes-gallery
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/themes-gallery.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Showcasing preset brand themes as cards that re-skin a live component preview when clicked.
- Letting visitors try different themes interactively on a docs or marketing page.
Avoid when
- Documenting individual token values rather than whole-theme presets; use style-guide instead.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
Usage
Render the block as a component, passing data where useful:
<x-blocks.themes-gallery />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable files ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
// Section copy (baked sample content — it's a block, not a primitive).
'eyebrow' => 'Theming',
'heading' => 'Pick a theme, see it live',
'subheading' => 'Eight curated presets. Click one to instantly re-skin the live preview — the same design tokens your whole app reads.',
])
{{--
Themes gallery. A grid of eight preset-theme cards drives a live component
preview rendered inside the block. Selecting a card binds that preset's token
channel values as inline custom properties on the preview wrapper
(:style="previewVars"), which cleanly scopes the theme to just the preview —
no <style> injection, no leakage. Values resolve for the current page mode
(light/dark) inside uiThemesGallery.
Every colour is bound at runtime from JS (dots via :style, preview via token
vars) — there are deliberately NO colour utilities or inline colour literals
in this markup.
--}}
<x-ui.section
x-data="uiThemesGallery()"
x-init="init()"
data-slot="themes-gallery"
space="none" {{ $attributes }}
>
<div class="mx-auto max-w-6xl px-4 py-16 sm:py-20">
<div class="mx-auto max-w-2xl text-center">
@if ($eyebrow)
<x-ui.badge variant="secondary" class="mb-4">{{ __($eyebrow) }}</x-ui.badge>
@endif
<h2 class="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
{{ __($heading) }}
</h2>
<p class="mx-auto mt-4 max-w-xl text-pretty text-base text-muted-foreground">
{{ __($subheading) }}
</p>
</div>
<div class="mt-12 grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,22rem)]">
{{-- Preset grid --}}
<div
role="group"
aria-label="{{ __('Preset themes') }}"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"
>
<template x-for="preset in presets" :key="preset.key">
<button
type="button"
@click="select(preset.key)"
:aria-pressed="isActive(preset.key) ? 'true' : 'false'"
:class="isActive(preset.key)
? 'border-transparent ring-2 ring-ring ring-offset-2 ring-offset-background'
: 'border-border hover:border-foreground/20'"
class="flex flex-col items-start gap-4 rounded-lg border bg-card p-6 text-start transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none"
>
<span class="flex items-center gap-2" aria-hidden="true">
<template x-for="(dot, i) in dots(preset)" :key="i">
<span
class="size-4 rounded-full border border-border/40 shadow-sm"
:style="`background-color: hsl(${dot})`"
></span>
</template>
</span>
<span class="text-sm font-medium text-foreground" x-text="preset.label"></span>
</button>
</template>
</div>
{{-- Live preview — themed by inline token vars scoped to this wrapper. --}}
<div
data-slot="themes-gallery-preview"
:style="previewVars"
aria-live="polite"
>
<x-ui.card class="overflow-hidden">
<x-ui.card.header>
<div class="flex items-center justify-between gap-4">
<x-ui.card.title>{{ __('Live preview') }}</x-ui.card.title>
<x-ui.badge x-text="activePreset.label">{{ __('Default') }}</x-ui.badge>
</div>
<p class="text-sm text-muted-foreground">
{{ __('Real primitives, re-skinned by the selected theme.') }}
</p>
</x-ui.card.header>
<x-ui.card.content class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-4">
<x-ui.button>{{ __('Primary') }}</x-ui.button>
<x-ui.button variant="secondary">{{ __('Secondary') }}</x-ui.button>
<x-ui.button variant="outline">{{ __('Outline') }}</x-ui.button>
</div>
<div class="flex flex-col gap-2">
<label for="themes-gallery-email" class="text-sm font-medium text-foreground">
{{ __('Email') }}
</label>
<x-ui.input
id="themes-gallery-email"
type="email"
name="email"
autocomplete="email"
placeholder="you@example.com"
/>
</div>
<div class="flex flex-wrap items-center gap-2">
<x-ui.badge>{{ __('Default') }}</x-ui.badge>
<x-ui.badge variant="secondary">{{ __('Secondary') }}</x-ui.badge>
<x-ui.badge variant="outline">{{ __('Outline') }}</x-ui.badge>
</div>
</x-ui.card.content>
</x-ui.card>
</div>
</div>
</div>
</x-ui.section>
/**
* Themes gallery — a live preset-theme switcher.
*
* Renders the generated brand catalog as colour-dot cards. Selecting one
* re-skins a live component preview rendered inside the block by binding the
* preset's design-token CHANNEL values (the `H S% L%` triples our utilities feed
* to `var(--token-color, hsl(var(--token)))`) as inline CSS custom properties on the preview
* wrapper. Inline style on that single wrapper scopes the theme to just the
* preview — there is no `<style>` injection and no leakage to the rest of the
* page.
*
* Each preset carries a LIGHT and a DARK token map. The `active` getter resolves
* to whichever matches the current page mode (the `.dark` class on
* <html>), so the preview always reflects how the theme reads in the mode the
* visitor is actually viewing. We observe the document root's class list so a
* mode toggle elsewhere on the page re-resolves the preview without a reload.
*
* Self-registers on `alpine:init` so import order does not matter; the glob
* importer picks this file up automatically.
*
* Cleanup: the only side effect is a MutationObserver on <html>; destroy()
* disconnects it so nothing leaks when Alpine tears the node down.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiThemesGallery', () => ({
// Generated preset set. Each preset has a label plus `light` / `dark`
// token maps of bare HSL channel triples (consumed via var(--x-color, hsl(var(--x)))).
// `secondary` / `accent` are only used to paint the card preview dots;
// the live preview themes via primary / primaryForeground / ring /
// radius. `radius` is mode-independent.
// <design-tokens:presets>
// Generated by scripts/design-tokens.mjs from tokens/brands/.
presets: [
{
"key": "default",
"label": "Default",
"radius": "1.5rem",
"light": {
"primary": "222.2 47.4% 11.2%",
"primaryForeground": "210 40% 98%",
"ring": "222.2 84% 4.9%",
"secondary": "225 22% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "210 40% 98%",
"primaryForeground": "222.2 47.4% 11.2%",
"ring": "212.7 26.8% 83.9%",
"secondary": "210 4% 10%",
"accent": "220 4% 11%"
}
},
{
"key": "zinc",
"label": "Zinc",
"radius": "0.5rem",
"light": {
"primary": "240 6% 10%",
"primaryForeground": "0 0% 98%",
"ring": "240 6% 10%",
"secondary": "240 5% 96%",
"accent": "240 5% 93%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "240 6% 10%",
"ring": "240 5% 84%",
"secondary": "240 4% 14%",
"accent": "240 4% 17%"
}
},
{
"key": "slate",
"label": "Slate",
"radius": "0.75rem",
"light": {
"primary": "215 25% 27%",
"primaryForeground": "210 40% 98%",
"ring": "215 25% 27%",
"secondary": "214 32% 91%",
"accent": "210 40% 94%"
},
"dark": {
"primary": "210 40% 90%",
"primaryForeground": "222 47% 11%",
"ring": "210 40% 80%",
"secondary": "217 33% 17%",
"accent": "217 33% 20%"
}
},
{
"key": "rose",
"label": "Rose",
"radius": "1rem",
"light": {
"primary": "346.8 77.2% 48%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "346.8 77.2% 48%",
"secondary": "350 100% 96%",
"accent": "350 100% 94%"
},
"dark": {
"primary": "346.8 77.2% 48%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "346.8 77.2% 48%",
"secondary": "345 25% 15%",
"accent": "345 30% 18%"
}
},
{
"key": "emerald",
"label": "Emerald",
"radius": "1rem",
"light": {
"primary": "142.1 76.2% 28%",
"primaryForeground": "355.7 100% 97.3%",
"ring": "142.1 76.2% 28%",
"secondary": "150 55% 94%",
"accent": "150 60% 92%"
},
"dark": {
"primary": "142.1 70.6% 45.3%",
"primaryForeground": "144.9 80.4% 10%",
"ring": "142.1 70.6% 45.3%",
"secondary": "150 25% 14%",
"accent": "150 28% 17%"
}
},
{
"key": "violet",
"label": "Violet",
"radius": "1.25rem",
"light": {
"primary": "262.1 83.3% 57.8%",
"primaryForeground": "210 20% 98%",
"ring": "262.1 83.3% 57.8%",
"secondary": "262 80% 96%",
"accent": "262 80% 94%"
},
"dark": {
"primary": "263.4 70% 50.4%",
"primaryForeground": "210 20% 98%",
"ring": "263.4 70% 55%",
"secondary": "262 30% 16%",
"accent": "262 32% 19%"
}
},
{
"key": "midnight",
"label": "Midnight",
"radius": "0.75rem",
"light": {
"primary": "224 76.3% 48%",
"primaryForeground": "210 40% 98%",
"ring": "224 76.3% 48%",
"secondary": "220 60% 95%",
"accent": "222 70% 93%"
},
"dark": {
"primary": "217.2 91.2% 59.8%",
"primaryForeground": "222.2 47.4% 11.2%",
"ring": "217.2 91.2% 59.8%",
"secondary": "220 35% 15%",
"accent": "220 40% 18%"
}
},
{
"key": "sunset",
"label": "Sunset",
"radius": "1rem",
"light": {
"primary": "24.6 95% 38%",
"primaryForeground": "60 9.1% 97.8%",
"ring": "24.6 95% 38%",
"secondary": "30 80% 93%",
"accent": "25 90% 90%"
},
"dark": {
"primary": "20.5 90.2% 48.2%",
"primaryForeground": "20 14.3% 4.1%",
"ring": "20.5 90.2% 48.2%",
"secondary": "20 25% 14%",
"accent": "20 30% 18%"
}
},
{
"key": "spotify",
"label": "Spotify",
"radius": "2rem",
"light": {
"primary": "141.6 73.4% 30%",
"primaryForeground": "0 0% 100%",
"ring": "141.6 73.4% 30%",
"secondary": "0 0% 95%",
"accent": "141 60% 92%"
},
"dark": {
"primary": "141.9 69.2% 58%",
"primaryForeground": "141 80% 8%",
"ring": "141.9 69.2% 58%",
"secondary": "0 0% 16%",
"accent": "141 30% 14%"
}
},
{
"key": "neo",
"label": "Neo",
"radius": "0rem",
"light": {
"primary": "0 0% 4%",
"primaryForeground": "0 0% 98%",
"ring": "0 0% 4%",
"secondary": "52 100% 60%",
"accent": "190 100% 85%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 4%",
"ring": "0 0% 98%",
"secondary": "52 100% 60%",
"accent": "190 80% 25%"
}
},
{
"key": "claude",
"label": "Claude",
"radius": "0.75rem",
"light": {
"primary": "15.2 63.1% 59.2%",
"primaryForeground": "20 50% 10%",
"ring": "15.2 63.1% 52%",
"secondary": "30 35% 91%",
"accent": "18 70% 90%"
},
"dark": {
"primary": "15.2 55.4% 52.5%",
"primaryForeground": "20 50% 10%",
"ring": "15.2 55.4% 52.5%",
"secondary": "20 15% 16%",
"accent": "18 30% 20%"
}
},
{
"key": "monogram",
"label": "Monogram",
"radius": "0rem",
"light": {
"primary": "0 0% 12.5%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 12.5%",
"secondary": "0 0% 95.7%",
"accent": "0 0% 95.7%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 12.5%",
"ring": "0 0% 83.9%",
"secondary": "0 0% 14.9%",
"accent": "0 0% 14.9%"
}
},
{
"key": "prestige",
"label": "Prestige",
"radius": "0rem",
"light": {
"primary": "0 0% 11%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 11%",
"secondary": "0 0% 88%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "0 0% 11%",
"ring": "0 0% 84%",
"secondary": "0 0% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "showcase",
"label": "Showcase",
"radius": "0rem",
"light": {
"primary": "240 9% 30%",
"primaryForeground": "0 0% 100%",
"ring": "240 9% 30%",
"secondary": "240 9% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "240 9% 92%",
"primaryForeground": "240 9% 12%",
"ring": "240 9% 84%",
"secondary": "240 8% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "madrid",
"label": "Madrid",
"radius": "0rem",
"light": {
"primary": "0 0% 0%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 0%",
"secondary": "0 0% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 98%",
"primaryForeground": "0 0% 0%",
"ring": "0 0% 84%",
"secondary": "0 0% 15%",
"accent": "220 4% 11%"
}
},
{
"key": "xclusive",
"label": "Xclusive",
"radius": "0rem",
"light": {
"primary": "0 0% 12%",
"primaryForeground": "0 0% 100%",
"ring": "0 0% 12%",
"secondary": "0 0% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "0 0% 12%",
"ring": "0 0% 84%",
"secondary": "0 0% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "alchemy",
"label": "Alchemy",
"radius": "0rem",
"light": {
"primary": "240 7% 11%",
"primaryForeground": "0 0% 100%",
"ring": "240 7% 11%",
"secondary": "240 6% 88%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "0 0% 95%",
"primaryForeground": "240 7% 11%",
"ring": "0 0% 84%",
"secondary": "240 7% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "reformation",
"label": "Reformation",
"radius": "0rem",
"light": {
"primary": "228 31% 23%",
"primaryForeground": "0 0% 100%",
"ring": "228 31% 23%",
"secondary": "228 30% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "228 30% 72%",
"primaryForeground": "228 30% 12%",
"ring": "228 30% 72%",
"secondary": "228 25% 16%",
"accent": "220 4% 11%"
}
},
{
"key": "sampo",
"label": "Sampo",
"radius": "0rem",
"light": {
"primary": "30 8% 19%",
"primaryForeground": "0 0% 100%",
"ring": "30 8% 19%",
"secondary": "30 8% 96%",
"accent": "228 14% 95%"
},
"dark": {
"primary": "30 8% 82%",
"primaryForeground": "30 8% 12%",
"ring": "30 8% 82%",
"secondary": "30 8% 16%",
"accent": "220 4% 11%"
}
}
],
// </design-tokens:presets>
activeKey: 'default',
// Bumped on a mode change so the `active`/`previewVars` getters recompute.
mode: 'light',
_observer: null,
init() {
this.mode = this._currentMode();
// Re-resolve the preview if the page mode toggles elsewhere.
this._observer = new MutationObserver(() => {
const next = this._currentMode();
if (next !== this.mode) this.mode = next;
});
this._observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
},
destroy() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
},
_currentMode() {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
},
// The currently selected preset (whole object).
get activePreset() {
return this.presets.find((p) => p.key === this.activeKey) || this.presets[0];
},
// Token map for the current preset resolved to the current page mode,
// plus the mode-independent radius.
get active() {
const p = this.activePreset;
// `this.mode` is read so this getter is reactive to mode changes.
const tokens = this.mode === 'dark' ? p.dark : p.light;
return { ...tokens, radius: p.radius };
},
// CSS custom properties bound inline on the preview wrapper. Setting the
// channel vars directly works because the utilities resolve them through
// var(--token-color, hsl(var(--token))); --radius is the raw length.
get previewVars() {
const a = this.active;
return {
'--primary': a.primary,
'--primary-foreground': a.primaryForeground,
'--ring': a.ring,
'--radius': a.radius,
};
},
// The three dots shown on a preset card, resolved for the current mode.
dots(preset) {
const tokens = this.mode === 'dark' ? preset.dark : preset.light;
return [tokens.primary, tokens.secondary, tokens.accent];
},
isActive(key) {
return this.activeKey === key;
},
select(key) {
this.activeKey = key;
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.5 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0