Skip to content

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.

Stylesheet-only (typical):

my-theme/
theme.yaml
my-theme.scss # or my-theme.css
icons/ # optional
home.svg

Stylesheet + 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.

manifestVersion: 1
metadata:
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.

  • Use SCSS variables for accent colors, then assign --crbn-primary and --crbn-primary-rgb together so JS backdrops can read RGB triplets from ctx.tokens.
  • Disk themes: Carbon compiles one entry file with sass.compileString. If you use multiple partials, run sass or bun locally and ship dist/theme.css with source: ./dist/theme.css, or merge partials into one .scss entry.
  • Repo built-ins: Vite inlines *.scss?inline and resolves @use / @import across the theme folder during app build.

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:

  1. Maintain src/theme.css with Tailwind layers or plugins that emit variables.
  2. Run the Tailwind CLI (or PostCSS) in your repo to produce dist/theme.css.
  3. Point the manifest at the compiled file:
configuration:
source: ./dist/theme.css

Example 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).

javascript:
enabled: true
entry: dist/theme.js
layer: backdrop

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: overlay

Keep 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.

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).

Import packages in src/, run Bun.build (or esbuild) to dist/theme.js, stay under 256 KB, no runtime import() or network.

Terminal window
bun init
bun add gl-matrix # example dev dependency
// build.ts: run with: bun run build.ts
const 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.

Add icons/*.svg and wire overrides like default-light in the repo.

  1. Stylesheet applies (#carbon-custom-theme in devtools).
  2. JS bundle passes shape + size checks.
  3. Switch themes twice; no stuck iframe.
  4. Resize window and background tab to confirm resize / pause behavior.
Page Link
Overview Themes
Authoring This page
Engine architecture Runtime pipeline
Manifest reference theme.yaml
Built-in catalog Shipped themes