Theming System
Inhalt
- The 6 Layers
- Layer 1: Primitive Tokens
- Layer 2: Semantic Tokens
- Layer 3: Admin Live-Editor
- Layer 4: Preset Themes
- Layer 5: Custom CSS
- Layer 6: Layout Blocks
- Live Update Mechanism
- Token Mapping Example
- Logo Filter Presets
- Custom CSS Tips
- Background gradient on the page
- Font override
- Radius step adjustment
- Full-bleed hero background
ITSWEBER Play features a 6-layer token architecture that enables live theme editing without page reloads. Every visual property — colors, radii, shadows, fonts — flows through a structured chain from raw values to rendered pixels.
The 6 Layers
Layer 1: Primitive Tokens
Location: packages/theme/tokens.json
Raw values with no semantic meaning. Colors, font families, border radii, shadow definitions. These are the foundation — they are never used directly in components. Components always reference semantic tokens (Layer 2).
{
"color": {
"neutral": { "50": "#f6f8fa", "…": "…", "900": "#0e1116" },
"teal": { "400": "#3ba7a7", "500": "#3ba7a7", "600": "#2d8a8a", "700": "#236e6e" },
"red": { "500": "#d9534f", "600": "#b43935" },
"amber": { "500": "#f0b429", "600": "#cb8e14" },
"green": { "500": "#3fb950", "600": "#2ea043" }
},
"radius": {
"sm": "6px",
"md": "10px",
"lg": "16px",
"full": "9999px"
},
"shadow": {
"card": "0 4px 20px rgba(0,0,0,.35)",
"lg": "0 12px 40px rgba(0,0,0,.45)"
},
"font": {
"sans": "'Geist', 'Geist Variable', system-ui, -apple-system, 'Segoe UI', sans-serif",
"mono": "'Geist Mono', 'Geist Mono Variable', ui-monospace, 'SF Mono', monospace"
}
}
Layer 2: Semantic Tokens
Location: packages/theme/styles/semantic.css (mirrored for JavaScript in packages/theme/src/semantic.ts)
Semantic tokens map named roles — bg, fg, brand, danger, warning — to primitive values. Tailwind classes and component styles reference only these tokens, never raw hex values.
| Semantic Token | Role | Maps to (dark default) |
|---|---|---|
--color-background |
Page background | --color-neutral-900 |
--color-surface |
Card / panel background | --color-neutral-800 |
--color-foreground |
Primary text | --color-neutral-100 |
--color-muted |
Secondary text | --color-neutral-300 |
--color-brand |
Accent / interactive | --color-teal-500 |
--color-brand-hover |
Hover state | --color-teal-400 |
--color-danger |
Errors, destructive actions | --color-red-500 |
--color-warning |
Warnings | --color-amber-500 |
--shadow-card |
Card elevation | fixed value in semantic.css |
// packages/theme/src/semantic.ts (excerpt)
export const semantic = {
bg: { base: "var(--color-neutral-900)", surface: "var(--color-neutral-800)" },
fg: { primary: "var(--color-neutral-50)", muted: "var(--color-neutral-400)" },
brand: { default: "var(--color-teal-500)", hover: "var(--color-teal-600)" },
intent: { danger: "var(--color-red-500)", warning: "var(--color-amber-500)" },
} as const;
Layer 3: Admin Live-Editor
Path: /admin/theme
A colorpicker and slider UI lets admins adjust semantic tokens at runtime. Changes are:
- Persisted in the Prisma model
ThemeSettings, fieldtokensOverride - Broadcast to all open tabs via Redis pub/sub and Server-Sent Events (
/api/theme/events)
No deployment or restart is required. See Live Update Mechanism below.
Layer 4: Preset Themes
Built-in named presets from packages/theme/presets/. Switchable in the admin panel with a single click. Custom presets are not stored in the database, but the current state can be exported and imported as JSON.
| Preset | Description | Best for |
|---|---|---|
itsweber-dark |
Default dark theme with teal accent | General use |
itsweber-light |
Light counterpart to itsweber-dark |
Bright environments |
itsweber-brand |
Dark navy with green accent after itsweber.de | Official instance |
high-contrast |
Maximum contrast for low vision | Accessibility |
retro |
80s synthwave with magenta and cyan on violet | Personal/niche instances |
Layer 5: Custom CSS
Freeform CSS input in the admin sandbox. Applied after all token layers, giving full override power. Features:
- Versioned: last 20 revisions retained, each restorable
- Checked: before saving, the CSS is checked for forbidden constructs (
@import,url(javascript:…),expression(),</style>,<script,behavior:) and limited to 100 KB. There is no syntax check - Scoped: applied inside a single
<style>tag, no framework classes required
See Custom CSS Tips for common patterns.
Layer 6: Layout Blocks
The homepage is composed from reorderable blocks, managed at /admin/page-blocks. Available block types:
| Block | Description |
|---|---|
| Hero | Full-width banner with title, subtitle, CTA button |
| Category-Chips | Scrollable category filter row |
| Video-Grid | Configurable grid of recent/featured videos |
| Shorts-Row | Row of short videos |
| Channel-Row | Row of channel cards |
| CTA-Banner | Call-to-action strip with custom text and link |
| Community-Row | Row of community posts |
Drag-and-drop reordering is supported. Changes take effect immediately without a page reload.
Live Update Mechanism
When an admin saves a theme change, the update propagates to all open browser sessions instantly:
Admin change → tRPC theme.update → DB save + Redis publish → SSE /api/theme/events
Frontend: replaces <style id="theme-vars"> with new CSS variables
Result: instant update, no React rerender required
The frontend keeps an EventSource connection to /api/theme/events. On receiving a theme:updated event, it re-fetches the current theme via tRPC and swaps the contents of <style id="theme-vars"> and <style id="theme-custom">. Because CSS variables cascade, every element using those variables updates in place — no component state change, no navigation.
Token Mapping Example
// tokens.json (primitives)
{
"color": {
"teal": { "500": "#3ba7a7" }
}
}
/* styles/semantic.css (semantic mapping) */
--color-brand: var(--color-teal-500);
/* tokens.json becomes: --color-teal-500: #3ba7a7 */
/* component usage */
.btn-primary {
background-color: var(--color-brand);
}
.btn-primary:hover {
background-color: var(--color-brand-hover);
}
Overrides address the primitives in dot notation. An admin setting color.teal.500 to #3b82f6 (blue) in the live editor writes { "color.teal.500": "#3b82f6" } to ThemeSettings.tokensOverride. This becomes --color-teal-500: #3b82f6 in <style id="theme-vars">, and --color-brand picks it up through the cascade.
Logo Filter Presets
The instance logo supports 10 CSS filter presets, applied via var(--logo-filter). This allows the logo to adapt to light/dark backgrounds without requiring separate image assets.
| Preset | CSS filter value | Use case |
|---|---|---|
| none | none |
No modification |
| glow | drop-shadow(0 0 12px var(--color-teal-500)) |
Glow in accent color |
| softGlow | drop-shadow(0 0 6px rgba(59,167,167,.6)) |
Subtle glow |
| duotone | grayscale(1) brightness(1.1) sepia(.3) hue-rotate(140deg) saturate(3) |
Single-tone tint |
| shadow | drop-shadow(0 4px 8px rgba(0,0,0,.5)) |
Shadow for depth |
| brightness0 | brightness(0) |
Solid black |
| invert | invert(1) |
Invert colors |
| invertGlow | invert(1) drop-shadow(0 0 12px var(--color-teal-500)) |
Inverted with glow |
| mono | grayscale(1) contrast(1.2) brightness(1.05) |
Grayscale |
| onlyWhite | brightness(0) invert(1) |
Solid white |
Custom CSS Tips
Background gradient on the page
body {
background: linear-gradient(135deg, var(--color-background) 0%, #1a0a00 100%);
}
Font override
:root {
--font-sans: 'Nunito', system-ui, sans-serif;
}
Radius step adjustment
:root {
--radius-md: 1.5rem;
}
Full-bleed hero background
/* adjust the selector to the real markup, the code sets no .hero-block class */
.hero-block {
background-image: url('/s3/play-assets/hero-bg.jpg');
background-size: cover;
background-position: center;
}
Safety: Custom CSS containing @import (to any target), url(javascript:…), url(data:text/html…), expression(), behavior:, <script or </style> is rejected with an error on save, not silently stripped. There is no syntax check, so broken CSS is saved and simply has no effect.