Add, refine, calibrate, or migrate OpenClaw Control UI themes by patching the installed bundled frontend assets in dist/control-ui/assets. Use when a user wants a new WebUI theme, wants colors matched to a reference theme such as VSCode, wants chat/component styling tuned inside a custom theme, or wants an existing custom theme preserved across an OpenClaw upgrade.
Add a new OpenClaw Control UI theme directly into the installed frontend bundle.
This skill also covers theme migration across OpenClaw upgrades. When a user already has a custom theme on an older installed bundle and wants to keep it after updating OpenClaw, preserve the old theme first, then migrate it into the new bundle using structural comparison rather than heuristic string injection.
Use this skill for installed bundle patching, not upstream source development.
Default rule: add a new theme from the user's requested palette. Do not overwrite built-in themes unless the user explicitly asks.
For migrations, use a stricter rule set:
Patch the active OpenClaw install only:
dist/control-ui/assets/index-*.jsdist/control-ui/assets/index-*.cssResolve the active install first. Do not assume the workspace copy is the live one.
For token design and live bundle patch locations, read as needed:
references/token-mapping.mdreferences/patch-points.mdreferences/theme-migration-checklist.md when doing a version-to-version preserve/rebuild of an existing custom themeBundled helper script:
scripts/backup_theme_bundle.py — backs up the active live JS/CSS bundles for a given theme id, extracts :root[data-theme=<id>] / :root[data-theme=<id>-light], and saves JS snippets for the allowed set, alias map, resolver, and theme card when foundIf the task is an upgrade or migration, follow the migration workflow below before any new patching. For day-of execution, prefer using references/theme-migration-checklist.md as the short operational checklist and this file as the fuller policy/guidance layer.
Use this when the user already has a custom theme on an older OpenClaw version and wants to keep it across an update.
Before updating OpenClaw:
Preferred method:
python3 scripts/backup_theme_bundle.py <theme-id> --output-dir /path/to/backups
Use manual extraction only if the helper script cannot capture enough of the live theme.
Leave the backup in the workspace, for example:
backups/openclaw-<theme-id>-theme/
report.json
<theme-id>.dark.css
<theme-id>.light.css
bundle-js.txt
bundle-css.txt
Minimum capture:
:root[data-theme=<theme-id>]:root[data-theme=<theme-id>-light] if presentAfter the OpenClaw update:
Compare the saved old bundle snippets to the new live bundle and answer these questions before editing:
Only patch after those answers are clear.
Migration rule:
Hard prohibition:
return e or first matching new Set(...) unless you have first confirmed that exact snippet is the theme resolver or set you intend to patchIf the migrated theme appears but looks wrong, or if unrelated UI behavior breaks:
Preferred recovery order:
Run:
which openclaw
readlink -f "$(which openclaw)"
openclaw status
Then locate candidate bundles:
find ~/.npm-global/lib/node_modules /usr/lib/node_modules /usr/local/lib/node_modules \
-path '*/openclaw/dist/control-ui/assets/index-*.js' -o \
-path '*/openclaw/dist/control-ui/assets/index-*.css'
If multiple installs exist, patch only the one that matches the active CLI path.
Collect the theme from one of:
Decide before patching:
If the theme should support the toggle, implement both dark and light selectors.
For both new themes and migrations, define not only the palette but also the intended component feel:
Do not assume token changes alone will always reproduce the intended message-bubble look in newer bundles.
Add:
:root[data-theme=<theme-id>]{...}
:root[data-theme=<theme-id>-light]{...}
Use an existing theme block as the structure template. Keep token names unchanged and only change values.
Use references/token-mapping.md to translate the user's palette into OpenClaw tokens.
At minimum define:
--bg, --bg-elevated, --panel, --card--text, --text-strong, --muted--border, --border-strong, --border-hover--accent, --accent-hover, --ring, --focusAlso define the usual supporting tokens already used by existing themes, including input, secondary, accent-2, state colors, and shadow/subtle variants.
If the desired result depends on specific chat bubble behavior, it is acceptable to add small theme-scoped component overrides after the root selectors, for example only under:
:root[data-theme=<theme-id>] .chat-line.user .chat-bubble{...}
:root[data-theme=<theme-id>] .chat-line.assistant .chat-bubble{...}
In newer OpenClaw bundles, verify which chat layout branch is actually live before calibrating. There may be more than one branch in the bundled CSS, commonly including .chat-group... and .chat-line... families. If both exist, inspect both and override the one that actually renders in the current UI; if needed, cover both branches under the same theme-scoped override.
Rules for component overrides:
Patch the bundled JS so the theme appears in Appearance settings and mode switching resolves correctly.
Use references/patch-points.md to locate the smallest reliable JS change points.
Update all relevant structures that actually exist in the current bundle:
Required behavior:
<theme-id><theme-id>-lightIf the bundle already contains an unused hidden theme id that matches the intended use, it is acceptable to expose and complete it instead of inventing another id.
For migrations, mirror the old theme's confirmed behavior, but express it through the new bundle's real structures. Do not port old JS fragments blindly into a new version.
After patching:
Ctrl+F5openclaw gateway restartSuccess means:
For chat-focused themes, explicitly verify both of these:
If the theme does not appear:
If the theme appears but looks unchanged:
If the light/dark toggle does not work:
<theme-id>-light is missing or misspelledIf the theme appears but looks wrong or breaks unrelated UI text/layout after a migration:
If the theme is structurally correct but some surfaces still feel off:
.chat-group... vs .chat-line...) is the one actually renderingLeave behind:
For migrations also leave behind: