Runtime

applyTheme changes a compiled site's token values in the browser, and refuses what would need a rebuild.

One function, synchronous

import { applyTheme } from 'uxdsl/runtime'

// Once, with the override the project was built with ({} for none):
applyTheme(projectOverride, { replace: true, styleId: 'uxdsl-ssr-theme' })

// Then any patch, any time:
const result = applyTheme({ palette: { primary: { main: 'color(green.700)' } } })
if (!result.ok) console.error(result.error.code, result.error.message)

applyTheme validates the patch, generates the theme CSS and compares its structure with what the build compiled, all before it touches the page. ok: true means the managed stylesheet already holds the new values; ok: false means nothing moved — no CSS, no state, no notification. The rest of the browser API is getAppliedTheme, resetTheme, subscribeTheme and loadPersistedTheme({ key }). uxdsl/runtime does not load PostCSS.

What it refuses

It swaps custom properties; it cannot rewrite the rules a build compiled into your components. So a patch that would change which declarations a directive emits is refused with UXD_THEME_STRUCTURE and the instruction to rebuild:

  • moving an existing breakpoint ({ breakpoints: { md: 900 } }) — every component's media queries carry it;
  • adding or removing a typography_details field, or a Button or Input state the role does not emit;
  • changing a Button's or Input's surface, or removing main/dark/contrast from a palette family used as a tone.

Token values, responsive expressions over the same breakpoints, dark-mode colors and new tokens apply. Before the first call, every other call is UXD_THEME_NOT_INITIALIZED.

On the server

There is no state to share: generateThemeCss(theme) from uxdsl/theme is pure and returns the same string the build emits for that theme, so a server can render it per request and the browser can adopt it.

import { generateThemeCss } from 'uxdsl/theme'

const css = generateThemeCss(effectiveTheme)   // render as <style id="uxdsl-ssr-theme">

Light and dark

Dark mode is part of the compiled theme, not a runtime patch: data-theme="dark" or "light" on <html> pins a mode, and with no attribute the OS preference decides. See Theme.

The theme this site applied

This site calls applyTheme(themes.default, { replace: true, styleId: 'uxdsl-ssr-theme' }) once, then applies the header's other themes the same way. These panels only read that state.

This site, right now

Theme selected in the header
default
getAppliedTheme() — families in the applied override
{} (not initialized yet)
getAppliedTheme().palette.primary.main
getComputedStyle(document.documentElement).getPropertyValue('--uxdsl__palette__primary-main') — the computed custom property

The two can differ: the override holds the light value, and in dark mode the stylesheet applyTheme generated switches primary-main to modes.dark's — which is what the computed style reads.

subscribeTheme(listener)

No application yet since this page loaded. Switch the theme in the header: each successful applyTheme notifies this listener.

Calls that change state run in a sandbox

On this page they would compete with the site's own theme, so they run in the frame below: a page compiled with this site's default theme, with its own document and its own copy of the runtime. Run the steps in order; applyTheme({ breakpoints: { md: 900 } }) is refused with UXD_THEME_STRUCTURE. Reload the sandbox to undo everything.

Theme API

  1. Before the project theme was applied once: refused, nothing moves.
  2. Initialize with the override the sandbox was compiled with (this site's default theme).
  3. A value change: applied, and saved under the lab's own key.
  4. Moving a threshold changes what the compiler emitted: refused with UXD_THEME_STRUCTURE.
  5. Back to the override it was initialized with — not the packaged base.
  6. The saved override comes back, validated like any patch.
  7. What is applied now, as a copy.
  8. Reset and forget the saved override.

One token at a time — still applyTheme

  1. A Color token: only color(gray.300) consumers follow. Where updateColor used to write an inline property, this replaces the custom property in the managed stylesheet.
  2. density(4) is space(5) at this width, so the Density box grows; the space(4) box does not.

What the sandbox reports

Calls, results, and what subscribeTheme heard

Run a step. Each call's return value is shown as it came back.