Theme system

The user-defined site theme — palette, styleGuide, fonts, modifiers — lives on ROOT.props (the [Background](components/background.md) node). The SDK reads it at runtime to generate CSS variables, scope them to the page container, and pai…

The user-defined site theme — palette, styleGuide, fonts, modifiers — lives on ROOT.props (the Background node). The SDK reads it at runtime to generate CSS variables, scope them to the page container, and pair with the FOUC pipeline so the first paint matches the design system.

Where theme data lives

FieldNotes
ROOT.props.palletArray of { name, color }. Drives palette CSS vars.
ROOT.props.styleGuideStyle tokens — radius, depth, font families, link/input colors. Built-in keys are catalogued in STYLE_TOKEN_REGISTRY; unknown keys are user-created (custom) tokens.
ROOT.props.theme.styleGuideMetaSparse sidecar Record<string, { appliesTo?, custom? }>. Marks user-created keys as custom: true so the picker shows Delete; optional appliesTo overrides the value-heuristic category for relevance filtering.
ROOT.props.themeThe effective theme (palette + styleGuide merged). Read via resolveTheme().
ROOT.props.theme.typographyArray of typography presets — see text-styles for the picker / editor + storage shape.
ROOT.props.modifiersSite-wide modifier classes applied at root.

End-user CRUD — every token (palette + typography + built-in styleGuide + custom) is created / edited / deleted via the unified VarPicker. The deprecated per-row dropdowns in StylesTab were removed; StylesTab now hosts only the global Density slider, the "Manage Tokens →" entry button, Breakpoints, and Editor preferences.

Key files

Var naming

toCSSVarName() converts camelCase to hyphenated CSS var names:

  • pallet[].name: "BrandAccent"--brand-accent
  • styleGuide.radiusBox--radius-box

Heading + Body fonts live in theme.typography[] as Heading and Body tokens. The orchestrator (generateDesignSystemCSSVariables) derives --heading-font-family, --heading-font-weight, --body-font-family, --body-font-weight from those tokens. Tailwind tokens font-heading / font-body reference the family vars directly. Any other *FontFamily key in styleGuide (e.g. accentFontFamily) auto-generates a CSS var and is recognized by extractGoogleFontsUrl for weight pairing.

Scoping

CSS vars are scoped to:

SurfaceSelector
Editor canvas#viewport
Viewer (/view/, custom domains, static)#ph-site-preview

Both surfaces also have fallback CSS vars in styles/editor.css matching the SDK default theme — these are safety nets so the canvas never flashes if SSR vars are absent. Values must match packages/sdk/src/theme.css.

Pipeline (SSR / FOUC)

generatePageFOUCData() in utils/generatePageFOUCData.ts extracts palette/styleGuide from CraftJS JSON, generates CSS vars, compiles the Tailwind classes used on the page, and returns Google Fonts URLs. <FOUCPreventionHead> renders the result into <Head>. Full pipeline in .claude/rules/css-architecture.md.

Init-time injection (SDK consumers)

External SDK consumers call init({ theme: {...} }). The injectTheme helper writes a <style id="pagehub-sdk-theme"> with CSS vars (--primary, --secondary, --accent, plus cssVariables) into the host container. This is for non-PageHub consumers — PageHub itself uses the FOUC pipeline.

Outline CTAs gotcha

If Primary and Base Content are both very dark neutrals (similar OKLCH lightness), btn btn-outline plus text-base-content reads as dark-on-dark — DaisyUI drives outline button foreground/border from --btn-color (often resolving to primary). Fix in theme data: separate lightness so primary is darker for fills and base-content is lighter for body. See .claude/rules/templates.md (Palette vs outline CTAs).

Related