Skinning Guide v2.0
Everything you need to build a MOLTamp skin. A skin is a folder with a JSON manifest and a CSS theme. No JavaScript, no build tools. For inspiration, browse the skins gallery or see what's possible with widgets.
Quick Start
cp -r ~/Moltamp/skins/_template ~/Moltamp/skins/my-skin - Edit
skin.jsonwith your info - Edit
theme.css— set your palette in:root, style elements withvar() - Drop art in
assets/, reference asurl('./assets/file.ext') - Open MOLTamp Settings → Skins, select your skin
The Rules
Non-negotiable. The validator enforces them.
1. All colors live in :root
Every color value must be a CSS variable in :root. Never hardcode a color in a selector.
/* WRONG */
.moltamp-panel-left { background: #070b07; }
/* RIGHT */
:root { --skin-panel-bg: #070b07; }
.moltamp-panel-left { background: var(--skin-panel-bg); } Why: Color overrides inject a :root block on top of your skin. Hardcoded colors can't be reached.
2. Contract variables come first
The 6 chrome and 21 terminal variables are the contract. Override them in :root. Extend with --skin-* prefixed variables.
3. Never hide panels
No display: none on panels. Users toggle visibility in Settings. Style them freely — visibility is the user's choice.
4. No executable content
- No external URLs (
https:,http:) - No
@import - No
expression(),javascript: - CSS only. Zero JS.
5. Don't hide the permission dialog
These trigger validator warnings: visibility: hidden, opacity: 0, pointer-events: none
6. No backgrounds on .moltamp-vibes
The vibes panel is always transparent. Visual content must come from GIF widget slots in skin.json, not CSS background. MOLTamp strips vibes backgrounds at load time.
7. Pseudo-elements need pointer-events: none
Every ::before and ::after must include pointer-events: none. Without it, they block right-click menus, drag handles, and widget interaction. Auto-fixed at load time, but declare it explicitly.
.moltamp-panel-right::before {
content: '';
position: absolute;
inset: 0;
background: var(--skin-overlay);
pointer-events: none; /* always include this */
} 8. Assets stay in assets/
- Reference with
url('./assets/filename.ext') - Supported images: PNG, JPG, WebP, GIF, SVG, AVIF
- Supported fonts: WOFF2, WOFF, TTF, OTF
- Max 10MB per file, 20MB per skin
theme.cssmax 100KB- No nested directories, no path traversal
9. Gate every visual effect behind an --effect-* variable
Every visual flourish must be controllable from the Effects panel. The validator scans your theme.css for six families and warns on upload when any of them isn't gated:
| Family | What it catches |
|---|---|
| Motion | animation, animation-name |
| Glow | box-shadow on any ::before/::after, box-shadow with blur ≥ 20px on regular elements, filter: drop-shadow() |
| Frost | filter: blur(), backdrop-filter |
| Overlay | mix-blend-mode, repeating-linear-gradient, repeating-radial-gradient, animated GIF/WebP/APNG backgrounds |
| Distortion | transform: perspective/skew/rotate3d/matrix3d, mask, clip-path |
| Color shift | filter: brightness/saturate/contrast/hue-rotate/invert/sepia/grayscale |
Declare the variable in :root and gate the effect so 0 means invisible or neutral. Effect variables run 0 (off) to 2 (200%), where 1 is your intended look.
:root {
--effect-mint-glow: 1;
}
.moltamp-terminal {
box-shadow: inset 0 0 calc(15px * var(--effect-mint-glow)) var(--skin-glow);
} 10. Don't declare dead toggles
Every --effect-X you declare in :root must be read by at least one rule via var(--effect-X). The Effects panel discovers toggles by scanning :root, so an unread variable renders a switch that does nothing. Either wire it up or remove it — the validator flags it either way.
Skin Format
skins/my-skin/
skin.json ← Required: manifest
theme.css ← Required: all styles
assets/ ← Optional: images, GIFs, SVGs skin.json
{
"id": "my-skin",
"name": "My Skin",
"version": "1.0.0",
"author": "Your Name",
"description": "A short description.",
"engine": "1.0"
} Required: id, name, version, engine. author and description are recommended but not enforced. id must match ^[a-zA-Z0-9_-]+$, and engine must be "1.0".
Optional manifest fields
| Field | Purpose |
|---|---|
tags | Freeform tags for search and filtering |
featured | Flags the skin for the Featured section (featuring is curated) |
effects | Labels and descriptions for your custom effects |
vibes | GIF widget slots for the vibes panel |
defaultLayout | The full layout your skin ships with — see below |
defaultLayout
Ship a layout and your skin arrives fully arranged: themed tab names, curated widgets, tuned panel sizes. Applied the first time a user selects your skin; they can override anything afterwards.
"defaultLayout": {
"tabAssignments": {
"intel": ["events", "files", "agents", "git"],
"signal": ["custom-1", "custom-2"]
},
"customTabs": {
"custom-1": { "label": "SENSORS", "widgets": ["system", "weather"] },
"custom-2": { "label": "COMMS", "widgets": ["music", "visualizer"] }
},
"dimensions": {
"leftWidth": 260, "rightWidth": 180, "vibesHeight": 180,
"bottomHeight": 26, "panelScale": 1, "terminalScale": 1
},
"panelVisibility": {
"showVibes": true, "showLeft": true, "showRight": true, "showBottom": true
},
"vibesDeck": {
"slots": [
{ "widgetId": "gif", "gifSrc": "assets/vibes.gif", "gifFit": "cover" },
{ "widgetId": "visualizer" }
],
"widths": [60, 40]
},
"visualizerPreset": "bars"
} vibesDeck is the full form of the vibes panel: the same slots as the top-level vibes block, plus a widths array of percentages controlling how the deck divides horizontally. If you omit it, MOLTamp falls back to vibes.slots at equal widths. visualizerPreset sets which visualizer the visualizer widget uses under your skin.
Variable Contract
Override these in :root. This is the API between MOLTamp and your skin.
Terminal Colors (21 vars)
| Variable | Purpose | Default |
|---|---|---|
--t-foreground | Main text | #e0e0e8 |
--t-background | Terminal background | #0a0a0f |
--t-cursor | Cursor color | #d4a036 |
--t-cursor-accent | Cursor text when selected | #0a0a0f |
--t-selection | Selection highlight | rgba(212,160,54,0.25) |
--t-black | ANSI 0 | #1a1a2e |
--t-red | ANSI 1 | #d43636 |
--t-green | ANSI 2 | #36d480 |
--t-yellow | ANSI 3 | #d4a036 |
--t-blue | ANSI 4 | #3678d4 |
--t-magenta | ANSI 5 | #a036d4 |
--t-cyan | ANSI 6 | #36b5d4 |
--t-white | ANSI 7 | #e0e0e8 |
--t-bright-black | ANSI 8 | #3a3a52 |
--t-bright-red | ANSI 9 | #ff5555 |
--t-bright-green | ANSI 10 | #50fa7b |
--t-bright-yellow | ANSI 11 | #f1c232 |
--t-bright-blue | ANSI 12 | #6299e6 |
--t-bright-magenta | ANSI 13 | #c678dd |
--t-bright-cyan | ANSI 14 | #56d4ef |
--t-bright-white | ANSI 15 | #ffffff |
Chrome Colors (6 vars)
| Variable | Purpose | Default |
|---|---|---|
--c-chrome-bg | Panel backgrounds | #0a0a0f |
--c-chrome-text | UI text | #9898a8 |
--c-chrome-border | Borders, dividers | #1a1a2e |
--c-chrome-accent | Highlights, active | #d4a036 |
--c-chrome-dim | Muted text | #606070 |
--c-chrome-hover | Hover backgrounds | #1e1e32 |
Effects (6 vars)
| Variable | Type | Purpose | Default |
|---|---|---|---|
--effect-scanlines | 0 or 1 | Scanline overlay | 0 |
--effect-glow | 0 or 1 | Inner glow | 0 |
--effect-crt | 0 or 1 | CRT curvature | 0 |
--scanline-opacity | 0-1 | Scanline intensity | 0.04 |
--glow-intensity | pixels | Glow blur | 12px |
--c-glow | color | Glow color | rgba(212,160,54,0.15) |
Typography & Layout
| Variable | Default |
|---|---|
--font-terminal | 'SF Mono', monospace |
--font-chrome | 'Inter', sans-serif |
--font-size | 14px |
--radius | 4px |
Targetable Elements
Layout
| Class | What |
|---|---|
.moltamp-shell | Root container |
.moltamp-titlebar | Top title bar |
.moltamp-vibes | Top banner — your canvas |
.moltamp-panel-left | Left panel (Intel) |
.moltamp-panel-right | Right panel (Signal) |
.moltamp-panel-bottom | Bottom bar (Telemetry) |
.moltamp-statusbar | Status bar |
Terminal
| Class | What |
|---|---|
.moltamp-terminal | Terminal container (xterm.js lives here) |
.moltamp-tabbar | Tab bar container |
.moltamp-tabbar-strip | Scrolling strip holding the session tabs |
.moltamp-tab | Individual session tab |
.moltamp-tab-active | Currently active tab |
The terminal is a real PTY, not a rendered transcript. Claude's output, tool calls, thinking blocks, code and your own typing are all terminal text painted by xterm.js — there are no message, tool-call, code-block or input elements to select. You style it by setting the ANSI palette (--t-foreground, --t-background, --t-black through --t-bright-white, --t-cursor, --t-selection) and by styling the .moltamp-terminal container itself.
Tabs & Attention Badges
MOLTamp 3.2.0 made every session a tab — up to 50 of them, in a strip that scrolls horizontally once they overflow rather than squeezing tabs into slivers. Cmd+P opens a searchable session manager.
A tab badges when its session needs the user, in one of two states:
| State | Color | Means |
|---|---|---|
| Waiting | var(--t-yellow) | The agent is blocked on you — permission prompt, or idle waiting for input |
| Done | var(--t-green) | The agent finished its turn while that tab was unfocused |
The colors are yours. Badge colors come straight from your terminal palette, so setting --t-yellow and --t-green restyles both the tab badge and the Cmd+P list for free. Pick those two with the tab bar in mind, not just the terminal — if your yellow disappears against your panel background, so does the badge.
The pulse animation is not yours. The badge breathes via a built-in animation applied as an inline style, so a skin can't slow it, speed it, or switch it off — and you shouldn't declare an --effect-* gate for it. It's a system signal, like the error flash: deliberately not skinnable, so a skin can never make "this session needs you" invisible. Style the tab chrome and let the badge do its job.
The Vibes Panel
The .moltamp-vibes div is your hero canvas.
Backgrounds are not allowed on .moltamp-vibes. The vibes panel is always transparent — all visual content (GIFs, images) must come from GIF widget slots defined in skin.json, not from CSS background or background-image. MOLTamp enforces this with an !important override at load time.
// skin.json
{
"vibes": {
"slots": [
{ "widgetId": "gif", "gifSrc": "assets/vibes.gif", "gifFit": "cover" }
]
}
} Use ::before and ::after for overlay effects — these are still allowed:
.moltamp-vibes::before {
content: '';
position: absolute;
inset: 0;
background: var(--skin-overlay);
mix-blend-mode: multiply;
pointer-events: none;
} Reactive Data Attributes
Set on .moltamp-shell — changes with Claude's state.
Activity Level
| Value | Meaning | Use for |
|---|---|---|
data-activity="idle" | No output | Dim, calm |
data-activity="low" | Some output | Moderate |
data-activity="high" | Streaming | Bright, intense |
Shell State
| Value | Meaning |
|---|---|
idle | Waiting for input |
thinking | Claude is reasoning |
streaming | Outputting text |
tool-use | Calling a tool |
permission | Waiting for permission |
error | Something broke |
Live CSS Properties
Set on .moltamp-shell for data-driven visuals:
| Property | Description |
|---|---|
--data-context-pct | Context window used % |
--data-cost-cents | Session cost in cents |
--data-tokens-in | Input tokens (thousands) |
--data-tokens-out | Output tokens (thousands) |
--data-rate-5h | 5-hour rate limit % |
--data-agents | Active subagent count |
--data-git-changed | Changed file count |
Example: Context gauge as pie chart:
.moltamp-context-gauge {
width: 80px; height: 80px;
border-radius: 50%;
background: conic-gradient(
var(--c-chrome-accent) calc(var(--data-context-pct) * 1%),
var(--c-chrome-border) 0
);
} Reactive Animations
Bind animations to data attributes:
[data-activity="high"] .moltamp-vibes {
filter: brightness(1.2) saturate(1.1);
animation: skin-glow 2s ease-in-out infinite;
}
[data-shell-state="thinking"] .moltamp-vibes {
animation: skin-pulse 1.5s ease-in-out infinite;
}
[data-shell-state="error"] .moltamp-vibes {
box-shadow: inset 0 0 20px var(--skin-error-color);
} Prefer transform and opacity for performance. Keep durations under 5s.
Preflight (Auto-Fixes)
MOLTamp applies these overrides after your skin CSS loads. They protect users from common mistakes — especially in AI-generated skins.
| What | Override | Why |
|---|---|---|
.moltamp-vibes background | background: transparent !important | Vibes visuals come from GIF widget slots |
All ::before / ::after | pointer-events: none !important | Pseudo-elements must never block clicks |
For AI-generated skins
If using ChatGPT, Claude, or another AI to generate a skin, paste this with your prompt:
Rules: All colors must be CSS variables in :root.
Use --skin-* prefix for custom vars.
Never set background on .moltamp-vibes — use skin.json vibes slots for images.
Every ::before/::after MUST have pointer-events: none.
No external URLs, no @import, no JS.
skin.json requires id, name, version, engine: "1.0".
theme.css max 100KB; assets max 10MB per file, 20MB total.
Gate EVERY effect behind an --effect-* variable (0 = off, 1 = default, 2 = max).
The validator checks six families:
MOTION — animation, animation-name
GLOW — box-shadow on ::before/::after, box-shadow blur >= 20px, drop-shadow
FROST — filter: blur(), backdrop-filter
OVERLAY — mix-blend-mode, repeating gradients, animated gif/webp/apng backgrounds
DISTORTION — transform: perspective/skew/rotate3d/matrix3d, mask, clip-path
COLOR SHIFT — filter: brightness/saturate/contrast/hue-rotate/invert/sepia/grayscale
Every --effect-X declared in :root must be READ by a rule via var(--effect-X).
The terminal is a raw xterm.js PTY. There are NO message, tool-call, thinking,
code-block or input elements — style it via the --t-* ANSI palette only.
Ship a defaultLayout in skin.json with themed tab names and curated widgets. Pre-Share Checklist
- All colors defined as variables in
:root - Zero hardcoded hex/rgb outside
:root - Contract variables (
--c-*,--t-*) overridden - Custom variables use
--skin-*prefix - No
backgroundorbackground-imageon.moltamp-vibes(use GIF widget slots) - Every
::before/::afterhaspointer-events: none - No external URLs or
@import - Every effect gated behind an
--effect-*variable (all six families) - No dead toggles — every
--effect-Xin:rootis read by a rule defaultLayoutinskin.jsonwith themed tab names--t-yellowand--t-greenstay legible against the tab bar (they drive the attention badges)- Assets under 10MB each, 20MB total;
theme.cssunder 100KB - Tested at different panel sizes
- Right-click context menus work on all panels
- Permission dialog still visible