Theme authoring
Step-by-step guide for theme authors. For manifest fields and limits, use theme.yaml reference. For how the app loads themes at runtime, see Engine architecture.
Folder layout
Section titled “Folder layout”Stylesheet-only (typical):
my-theme/ theme.yaml my-theme.scss # or my-theme.css icons/ # optional home.svgStylesheet + JavaScript:
my-theme/ theme.yaml my-theme.scss src/ theme.ts dist/ theme.js # required at runtime (bundled) package.json # optional: bun/npm for dev build.ts # optional: Bun.build for JS (and/or CSS)Disk themes must set configuration.source to the stylesheet path (for example ./my-theme.scss). JS themes set javascript.entry (for example dist/theme.js). Carbon reads files through the backend; it never runs bun install or node_modules inside the app.
Install: copy the folder to Skeleton/Themes/my-theme/ (folder name = id), then switch theme in Settings → Appearance.
Minimal token theme (SCSS)
Section titled “Minimal token theme (SCSS)”manifestVersion: 1metadata: name: My Theme description: Cool cobalt panels on Mica. author: your-name version: 1.0.0 tags: [dark]configuration: backdrop: type: mica theme: dark source: ./my-theme.scss colors: primary: "#5b9fff" background: "#080a10" surface: "#0c0f18" text: "#eef1f6":root { --crbn-bg-deep: transparent; --crbn-bg: rgba(8, 10, 16, 0.72); --crbn-bg-surface: rgba(12, 15, 24, 0.9); --crbn-text: #eef1f6; --crbn-text-muted: #788698; --crbn-primary: #5b9fff; --crbn-primary-rgb: 91, 159, 255; --crbn-border: rgba(91, 159, 255, 0.14);}Copy the full token set from default-dark or deep-lattice in project/frontend/core/src/themes/ so every control, sidebar, title bar, and status region stays consistent.
SCSS tips
Section titled “SCSS tips”- Use SCSS variables for accent colors, then assign
--crbn-primaryand--crbn-primary-rgbtogether so JS backdrops can read RGB triplets fromctx.tokens. - Disk themes: Carbon compiles one entry file with
sass.compileString. If you use multiple partials, runsassorbunlocally and shipdist/theme.csswithsource: ./dist/theme.css, or merge partials into one.scssentry. - Repo built-ins: Vite inlines
*.scss?inlineand resolves@use/@importacross the theme folder during app build.
Using Tailwind (build time only)
Section titled “Using Tailwind (build time only)”Carbon does not process Tailwind when loading a theme. The shell is styled with --crbn-* custom properties, not Tailwind utilities in markup.
You can still use Tailwind as an authoring tool:
- Maintain
src/theme.csswith Tailwind layers or plugins that emit variables. - Run the Tailwind CLI (or PostCSS) in your repo to produce
dist/theme.css. - Point the manifest at the compiled file:
configuration: source: ./dist/theme.cssExample input (compile with your tailwind.config):
@tailwind base;
@layer base { :root { --crbn-primary: theme(colors.sky.400); --crbn-primary-rgb: 56, 189, 248; --crbn-bg-surface: color-mix(in srgb, theme(colors.zinc.950) 90%, transparent); }}Do not rely on @apply flex in a theme file to change Carbon components, those components do not use your utility classes. Do use Tailwind to generate token values or global :root rules you compile ahead of time.
The same pattern works for PostCSS, Lightning CSS, or Sass output: ship final CSS (or a single SCSS entry without Tailwind directives).
Manifest with JavaScript backdrop
Section titled “Manifest with JavaScript backdrop”javascript: enabled: true entry: dist/theme.js layer: backdroplayer: backdrop vs overlay (frontdrop)
Section titled “layer: backdrop vs overlay (frontdrop)”There is no frontdrop key in the manifest, the implemented values are backdrop and overlay (themeBootstrap.ts → THEME_LAYER_Z_INDEX).
layer |
Stacking | Mica while JS is on | Notes |
|---|---|---|---|
backdrop |
Behind .app-container (z-index 0) |
Off (so the canvas shows through) | Default for Frosted, Slipstream, Deep Lattice |
overlay |
Above the shell (z-index 9000) |
Can stay on per configuration.backdrop |
Use when you want the effect in front of Carbon UI |
Switch from backdrop to “frontdrop” by changing one line:
javascript: enabled: true entry: dist/theme.js layer: overlayKeep token SCSS semi-transparent if you still want to read controls underneath. The host container is always pointer-events: none; set interactive: true only if you need pointer data forwarded into the theme (desktop frontend).
Backend tests accept layer: overlay in theme.yaml; no built-in theme ships with overlay yet, it is supported in the host.
JavaScript entry (required shape)
Section titled “JavaScript entry (required shape)”CarbonTheme.mount(function (ctx) { var g = ctx.canvas.getContext('2d'); if (!g) return; ctx.events.on('init', function () { /* start loop */ });});Honor ctx.settings.paused and ctx.settings.reducedMotion.
Carbon does not ship a WebGL API, use the DOM inside the iframe. Pattern from deep-lattice:
CarbonTheme.mount(function (ctx) { ctx.canvas.style.display = 'none';
var glCanvas = document.createElement('canvas'); glCanvas.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;'; ctx.root.appendChild(glCanvas);
var gl = glCanvas.getContext('webgl', { alpha: true, antialias: true, depth: true }) || glCanvas.getContext('experimental-webgl'); if (!gl) return;
ctx.events.on('resize', function () { var dpr = ctx.settings.dpr || 1; glCanvas.width = Math.floor(ctx.settings.width * dpr); glCanvas.height = Math.floor(ctx.settings.height * dpr); glCanvas.style.width = ctx.settings.width + 'px'; glCanvas.style.height = ctx.settings.height + 'px'; gl.viewport(0, 0, glCanvas.width, glCanvas.height); }); ctx.events.on('init', function () { /* start rAF loop */ });});Read accent colors from ctx.tokens (for example --crbn-primary-rgb as a comma-separated triplet).
Bundling with Bun or npm
Section titled “Bundling with Bun or npm”Import packages in src/, run Bun.build (or esbuild) to dist/theme.js, stay under 256 KB, no runtime import() or network.
bun initbun add gl-matrix # example dev dependency// build.ts: run with: bun run build.tsconst result = await Bun.build({ entrypoints: ['./src/theme.ts'], outdir: './dist', minify: true, target: 'browser', format: 'iife', naming: 'theme.js',});if (!result.success) throw new Error('Bundle failed');Your source must still expose a top-level CarbonTheme.mount(function (ctx) { ... }); after bundling.
| Works when bundled | Blocked |
|---|---|
| gl-matrix, three (tree-shaken), simplex-noise, culori, gsap (tree-shaken) | fetch, workers, eval, postMessage in theme code, storage, window.parent |
Libraries that lazy-load chunks or WASM from a URL fail unless you inline everything at build time.
Full API, token list, and guard rules: reference.
Icons (optional)
Section titled “Icons (optional)”Add icons/*.svg and wire overrides like default-light in the repo.
Test checklist
Section titled “Test checklist”- Stylesheet applies (
#carbon-custom-themein devtools). - JS bundle passes shape + size checks.
- Switch themes twice; no stuck iframe.
- Resize window and background tab to confirm
resize/pausebehavior.
Themes documentation (sidebar)
Section titled “Themes documentation (sidebar)”| Page | Link |
|---|---|
| Overview | Themes |
| Authoring | This page |
| Engine architecture | Runtime pipeline |
| Manifest reference | theme.yaml |
| Built-in catalog | Shipped themes |