CSS Tokens and Themes #
Style components with ordinary CSS custom properties. You can define them in
your own stylesheets, or use --theme to validate and supply a shared token
set. Theming does not scope selectors: Light DOM CSS is global in its owning
CSS tree; Shadow DOM provides native isolation.
/* my-card.css, using the default Shadow DOM mode */
:host {
color: var(--text-primary);
background: var(--surface-page);
}
Configure a theme #
Theme files map token names without -- to CSS value strings:
{
"themes": {
"light": {
"text-primary": "#111827",
"surface-page": "#ffffff"
},
"dark": {
"text-primary": "#fafafa",
"surface-page": "#171717"
}
}
}
A flat object such as { "text-primary": "#111827" } defines one theme named
default. Load a local JSON file or an installed npm package:
webui build ./src --out ./dist --theme ./themes/brand.json
webui serve ./src --state ./data/state.json --theme ./themes/brand.json --watch
For a package, --theme @my-org/brand-tokens resolves its tokens.json;
@my-org/brand-tokens/custom.json selects another file.
Apply theme values #
webui build --theme validates token coverage; it does not choose the active
theme or add a theme switcher. webui serve --theme also puts resolved CSS
declarations in state as tokens.<theme-name>. Apply them in the entry
template's <head>:
<style>
:root {
color-scheme: light dark;
/*{{{tokens.light}}}*/
}
@media (prefers-color-scheme: dark) {
:root {
/*{{{tokens.dark}}}*/
}
}
</style>
Use tokens.default for a flat theme file. The CSS-comment wrapper around each
binding is required. These are raw CSS declarations: load trusted theme files,
not user-supplied strings.
In a custom host, resolve tokens and supply the resulting declarations in
render state, or publish them as a stylesheet. The Rust
microsoft-webui-tokens crate exposes load_token_file, resolve_tokens,
and inject_token_css; other hosts can read the protocol's token inventory
through their integration API.
Required tokens and defaults #
With --theme, WebUI checks the var(--name) references in your component
CSS and inline <style> blocks. Provide those tokens in each theme, define
local defaults, or give the usage a literal fallback.
Nested Fallbacks #
Without local defaults, all three names in this example need theme values:
.card {
color: var(--a, var(--b, var(--c)));
}
Use a literal fallback such as var(--gap, 16px) when the token is optional.
Malformed var() calls fail the build.
Local Definition Exclusion #
A definition in the same declaration block provides a local default.
Unconditional :host or :root defaults also apply to descendant components:
:host {
--card-spacing: 8px;
padding: var(--card-spacing);
color: var(--designSystemColor);
}
Here, the theme needs designSystemColor, but not card-spacing.
Selector-scoped overrides do not replace theme defaults outside their matching
elements. For example, .green { --brand: var(--green-brand); } still requires
the theme's brand token when another rule or child component uses
var(--brand). Conditional defaults inside @media, @supports, or
@container likewise do not replace unconditional defaults. Put shared
Shadow component defaults in a bare :host rule and document defaults in
:root; these can also live inside cascade layers. :host is not valid in
Light component CSS.
Theme Validation #
When a build is given a theme (webui build --theme, webui serve --theme, or
API build options with a theme), every required token must exist in every
theme. For var(--a, var(--b, var(--c))), the theme must provide a, b, and
c unless those references are covered by same-block definitions or
unconditional local/ancestor defaults. Missing
tokens fail with missing-theme-token; theme token values that reference an
undefined or cyclic var(--token) are trusted and left to browser CSS
semantics.
Errors point at the CSS that needs attention and suggest a matching theme token:
✘ error: missing theme token [missing-theme-token]
--> my-card.css:2:10
color: var(--color-neutral-2000);
help: did you mean --color-neutral-200? otherwise define it locally
A var() usage that supplies a literal CSS fallback is exempt: the literal
already provides a value, so the token is not required:
:host {
color: var(--brand, #000); /* not required: #000 is the fallback */
margin: var(--gap); /* required: no fallback */
}
The theme can still supply brand; the literal is used when no value is
available. If another usage has no fallback, brand becomes required.
A token used only with a literal fallback but missing from every theme produces
a non-fatal typo advisory. webui build prints it; library callers can inspect
BuildResult::warnings.
Comment Handling #
HTML and CSS comments are stripped at build time. Bindings inside HTML comments
are ignored, so comments cannot be used as token placeholders. CSS legal
comments are preserved only when --legal-comments inline is active.
Unterminated HTML or CSS comments fail the build.
Use the CSS-comment-wrapped theme bindings shown under Apply theme values. The comment must contain exactly one binding.
For inspecting a compiled build, see the CLI reference.