Pagelet/ docs
Docs menu

Theming & color modes

One JSON block drives the whole look — shadcn-named variables, dark mode built in.

The theme block

A pagelet's look is driven by a small JSON theme block compiled to CSS variables that mirrorshadcn's names — so a brand's design system drops in without reinvention. Minimal is fine: set just primary and everything else inherits sensible defaults.

theme block
<script type="application/json" data-pagelet="theme">
{
  "colors": {
    "primary": "#4f46e5",
    "background": "#ffffff",
    "foreground": "#0a0a0a",
    "muted": "#f1f5f9",
    "border": "#e5e7eb"
  },
  "dark": { "background": "#0a0a0a", "foreground": "#fafafa" },
  "radius": "0.5rem",
  "fontFamily": "Inter, system-ui, sans-serif"
}
</script>
KeyCompiles toNotes
colors.*:root { --<name>: … }shadcn names: background, foreground, card, primary, secondary, muted, accent, border, ring (+ their *-foreground pairs)
dark.*[data-theme="dark"] { … }Per-variable dark-mode overrides — only list what changes
radius--radiusAny CSS length
fontFamilybody fonttypography.fontFamily also accepted

Accepted aliases, so existing brand JSON maps on directly:

AliasResolves to
textforeground
text-mutedmuted-foreground
surfacecard
typography.fontFamilyfontFamily

The color-mode model

  • Initial mode follows the OS (prefers-color-scheme). <html data-theme="dark"> forces dark.
  • The toolbar toggle cycles auto → light → dark and persists when localStorage is available.
  • The resolved mode sets color-scheme to match (form controls, scrollbars follow).
  • Every change fires a pagelet:themechange event on document — listen for it if your own JS paints anything mode-dependent (e.g. a canvas).

Custom CSS must be mode-aware

The toggle flips <html data-theme> at any time. If your page ships a<style> block with hardcoded light-mode colors, the content stays light while the runtime chrome goes dark — or text ends up dark-on-dark. Both modes must be readable:

  1. Prefer the theme block for anything the runtime styles (prose, code, tables, toolbar).
  2. Scope custom CSS by mode. Dark overrides go behind [data-theme="dark"].
  3. Never hardcode a light-only palette. A deliberately branded surface (a navy hero) must pin its own text colors so it reads in both modes.
  4. Don't repurpose the runtime's token names. The runtime owns --background, --foreground, --card, --primary, --secondary, --muted, --accent, --border, --ring and their *-foreground pairs on :root — and it loads after your CSS, so it wins ties. Consume the runtime tokens or namespace your own (--brand-ink).
good — namespaced tokens, dark overrides scoped
:root { --paper: #ffffff; --ink: #101828; }
[data-theme="dark"] { --paper: #12141c; --ink: #e6e8f0; }
.panel { background: var(--paper); color: var(--ink); }
bad — light-only palette + clobbered runtime token
/* light-only palette — breaks the toolbar's dark mode */
.panel { background: #ffffff; color: #101828; }

/* repurposing a runtime token — the runtime loads after your
   CSS and owns --muted (a SURFACE color, not muted text) */
:root { --muted: #525252; }

Test both modes before publishing: click the toolbar theme toggle once and scan for vanished text or inverted contrast. This is the single most common quality failure.

Deriving a theme from a brand

  • If you can read the user's codebase, lift their existing CSS variables / Tailwind tokens / brand colors and map them onto the names above — don't invent a palette.
  • Otherwise ask for one or two colors (primary, maybe accent) and let the defaults fill in.
  • On the hosted service, the MCP extract_brand tool scrapes a public brand URL and returns a ready-made theme (palette + radius) — feature-detect via service.brand_extract.
  • Keep contrast accessible; one accent used sparingly beats a rainbow.

The same theme block works on any template and any bring-your-own-HTML pagelet. A future hosted-theme reference (data-ref="…") will let you pull a saved brand theme; it's feature-detected and not available yet.