theme.yaml reference
Current schema version: manifestVersion: 1.
Manifest file: theme.yaml (preferred) or manifest.json. Unknown root fields are rejected for disk themes validated by the backend.
Top-level fields
Section titled “Top-level fields”| Field | Required | Description |
|---|---|---|
manifestVersion |
yes | Must be 1. |
metadata |
yes | Display name, description, author, version, tags, optional github. |
configuration |
yes* | Backdrop, stylesheet path, preview colors. |
javascript |
no | Sandboxed interactive backdrop. |
* Disk themes must set configuration.source. Built-in repo themes embed CSS in the app binary.
metadata
Section titled “metadata”| Field | Required | Description |
|---|---|---|
name |
yes | Theme picker title. |
description |
yes | Short subtitle. |
author |
yes | Author or team. |
version |
no | Semver (recommended). |
github |
no | Source repository URL. |
tags |
no | Strings such as dark, animated, webgl. |
configuration
Section titled “configuration”| Field | Required | Description |
|---|---|---|
source |
yes (disk) | Relative .scss, .sass, or .css. See Stylesheets. |
backdrop |
no | Windows shell material (Tauri). |
colors |
no | Picker preview: primary, background, surface, text. |
configuration.backdrop
Section titled “configuration.backdrop”| Field | Values | Description |
|---|---|---|
type |
mica, mica-alt, mica_alt, acrylic, blur, none |
Window backdrop effect. |
theme |
light, dark |
Mica light/dark tint. |
If javascript.enabled is true and javascript.layer is backdrop, Carbon does not apply Mica so the iframe canvas is visible behind the shell. If layer is overlay, Mica/backdrop from configuration.backdrop may still apply.
javascript
Section titled “javascript”| Field | Required | Description |
|---|---|---|
enabled |
yes | Enable sandbox iframe. |
entry |
yes if enabled | Relative path to one JS file. Max 256 KB (ThemeManifestSchema.MaxJavascriptBytes). |
layer |
no | Where the iframe sits in the stack (see below). Default: backdrop. |
interactive |
no | Desktop frontend only: forward pointer data as app events (kind: 'pointer'). Supported in bundled manifest.json in the repo. Not in the C# ThemeJavascriptSettings model, theme.yaml on disk cannot persist this field through the backend yet. |
javascript.layer (backdrop vs overlay / “frontdrop”)
Section titled “javascript.layer (backdrop vs overlay / “frontdrop”)”| Value | Implementation | Description |
|---|---|---|
backdrop |
THEME_LAYER_Z_INDEX.backdrop = 0 |
JavaScript draws behind the Carbon UI. Disables Mica while active. |
overlay |
THEME_LAYER_Z_INDEX.overlay = 9000 |
JavaScript draws in front of the shell (colloquial “frontdrop”). Does not automatically disable Mica. |
Invalid values are rejected at parse time (javascript.layer must be 'backdrop' or 'overlay').
Example overlay manifest (from backend tests):
javascript: enabled: true entry: dist/theme.js layer: overlayThe iframe is mounted with position: fixed; inset: 0 and pointer-events: none unless the host enables the interactive pointer bridge.
Entry shape
Section titled “Entry shape”After trim, the file must match:
CarbonTheme.mount(function (ctx) { ... });Validated by validateThemeJavascript before injection.
Forbidden JavaScript patterns
Section titled “Forbidden JavaScript patterns”validateThemeJavascript() strips comments and string literals, then applies these rules (themeJavascriptGuard.ts):
| Rule id | Blocked |
|---|---|
postMessage |
postMessage in theme source (wrapper owns parent I/O) |
window-parent |
window.parent, top, opener, frames |
parent-top-ref |
parent.postMessage, parent.document, storage on parent, etc. |
message-listener |
addEventListener('message'), onmessage = |
broadcast |
BroadcastChannel, MessageChannel, MessagePort |
network |
fetch(, XMLHttpRequest, WebSocket, EventSource |
dynamic-import |
import( |
workers |
Worker, SharedWorker, ServiceWorker |
eval |
eval(, new Function( |
storage |
localStorage, sessionStorage, indexedDB, openDatabase |
cookie |
document.cookie |
host-bridge |
__TAURI__, invoke(, webview, chrome., dotnet |
nested-frame |
createElement('iframe'), srcdoc=, javascript: URLs |
script-breakout |
</script in source |
Do not rely on hiding APIs inside strings, the stripper removes most literals before scan.
Iframe sandbox
Section titled “Iframe sandbox”- Attribute:
sandbox="allow-scripts"(noallow-same-origin). - CSP: no
connect-src, no workers, inline scripts only. - Theme code runs in
srcdocwith injectedCarbonThemewrapper first.
Stylesheets: SCSS, CSS, Tailwind
Section titled “Stylesheets: SCSS, CSS, Tailwind”| Format | When Carbon loads the theme |
|---|---|
.css |
Injected into #carbon-custom-theme unchanged. |
.scss, .sass |
Compiled with Dart Sass (compileString), then injected. |
Tailwind (@tailwind, @apply in unprocessed files) |
Not supported at load time. Compile to .css in your dev workflow and set source to the output. |
Carbon never runs PostCSS, Tailwind CLI, or node_modules when applying a theme. SCSS is first-class; Tailwind is a valid authoring tool only if you ship compiled CSS (or plain SCSS without Tailwind directives).
Disk themes compile a single source file per load. Multi-file SCSS partials require a local build step or a monolithic entry file.
ctx API
Section titled “ctx API”Used inside CarbonTheme.mount(function (ctx) { ... }):
| Member | Description |
|---|---|
ctx.canvas |
Full-viewport 2D canvas; host sets DPR transform on resize. |
ctx.root |
Full-viewport div for extra DOM (WebGL canvas, etc.). |
ctx.settings.width / height / dpr |
Viewport size. |
ctx.settings.reducedMotion |
From prefers-reduced-motion. |
ctx.settings.paused |
True when document is hidden or host sent pause. |
ctx.settings.seed |
Numeric seed from host. |
ctx.tokens |
Map of --crbn-* computed values (strings). |
ctx.random |
Seeded mulberry32 PRNG. |
ctx.events.on(type, fn) |
Subscribe; returns unsubscribe function. |
Host events (ctx.events.on)
Section titled “Host events (ctx.events.on)”| Type | When |
|---|---|
init |
After mount; includes settings, seed, tokens. |
resize |
Window size or DPR change; default canvas resized. |
pause / resume |
Tab visibility. |
destroy |
Before iframe teardown. |
tokens |
--crbn-* updated after CSS change. |
app |
Host event messages (see below). |
Themes must not register message listeners or call postMessage; the wrapper owns the bridge.
app event payloads (host → theme)
Section titled “app event payloads (host → theme)”Delivered as ctx.events.on('app', function (ev) { ... }) when the host sends type: 'event':
| Payload | Meaning |
|---|---|
{ type: 'theme-activated', themeId } |
Iframe passed validation and sent ready |
{ type: 'theme-deactivated', themeId } |
Theme is being torn down |
{ kind: 'pointer', phase, x, y, buttons, pressure } |
When javascript.interactive is true (phase: down / move / up / cancel) |
Token reference (--crbn-*)
Section titled “Token reference (--crbn-*)”Themes set CSS variables on :root. The shell reads these names (not Tailwind classes). Minimum useful set:
| Tokens | Purpose |
|---|---|
--crbn-bg-deep, --crbn-bg, --crbn-bg-surface |
Layered backgrounds |
--crbn-*-rgb variants |
Effects and JS ctx.tokens parsing |
--crbn-text, --crbn-text-muted |
Body copy |
--crbn-primary, --crbn-primary-rgb, --crbn-primary-hover |
Accent |
--crbn-border, --crbn-border-hover |
Outlines |
--crbn-error, --crbn-warning, --crbn-success, --crbn-info |
Status |
--crbn-tb-* |
Title bar |
--crbn-nb-* |
Sidebar / nav icons |
--crbn-bb-* |
Status bar |
--crbn-glass-opacity, --crbn-glass-blur |
Glass chrome |
Full list: project/frontend/core/src/themes/default-dark/default-dark.scss.
npm packages in dist/theme.js
Section titled “npm packages in dist/theme.js”There is no curated allow list. Bundled code must pass the static guard and fit in 256 KB.
| Kind | Examples | Notes |
|---|---|---|
| 3D / WebGL | three, gl-matrix, twgl.js, regl | Prefer tree-shaking; size is the limit. |
| Noise / motion | simplex-noise, @tweenjs/tween.js | Canvas motion without Three. |
| Color | culori, chroma-js | Parse/mix token colors. |
| Animation | gsap (core, tree-shaken) | Watch bundle size. |
| Heavy engines | pixi.js, phaser | Usually exceed cap. |
Use Bun or npm only at build time: bun add → import in src/ → Bun.build → dist/theme.js. Carbon only reads the bundle string.
Host lifecycle
Section titled “Host lifecycle”| Mechanism | Default |
|---|---|
| Ready timeout | 5 s without ready → remove theme |
| Watchdog ping | 15 s interval; 2 missed pongs → teardown |
| iframe navigation | Security teardown |
| Theme switch | destroy + iframe removal (500 ms grace) |
See Theme engine architecture for the full pipeline and source file map.
Backend API (desktop)
Section titled “Backend API (desktop)”The frontend calls generated ThemesApi methods (XPUI):
| Method | Purpose |
|---|---|
ListDiskThemes() |
Manifest metadata for folders under Skeleton/Themes/ |
ReadStylesheet(themeId) |
Raw text of configuration.source |
ReadJavascript(themeId) |
Raw text of javascript.entry (size-limited) |
Stylesheet compilation happens in the browser (compileDiskThemeStylesheet), not on the server.
Path security (disk)
Section titled “Path security (disk)”sourceandjavascript.entrymust be relative paths inside the theme folder...segments and path escape are rejected.
Versioning
Section titled “Versioning”Bump metadata.version when changing CSS or JS. Theme id = directory name under Skeleton/Themes/.
Themes documentation (sidebar)
Section titled “Themes documentation (sidebar)”| Page | Link |
|---|---|
| Overview | Themes |
| Authoring | Create a theme pack |
| Engine architecture | Runtime pipeline |
| Manifest reference | This page |
| Built-in catalog | Shipped themes |