Skip to main content

Custom Theme Authoring Guide

monodit has a two-track theme system:

  • Basic Theme — a set of static design tokens (colors, typography, border radius) applied as CSS variables. Simple, fast, portable.
  • Dynamic Theme — custom HTML/CSS/JS running in a sandboxed iframe as an invisible background layer. For animated backgrounds, interactive visuals, and complex aesthetics.

Both tracks are packaged together in a single .monodit-theme.md manifest file.

Pro feature. Creating and saving custom themes requires a Pro account. Free users can apply Official Themes only.


The Theme Manifest Format

A theme file is a .monodit-theme.md file. Metadata lives in YAML frontmatter; for Dynamic Themes, three fenced code blocks (html, css, js) hold the background-layer source:

---
monodit: theme
specVersion: "3"
themeType: dynamic
name: My Theme
description: A short description
version: "1.0.0"
author: Your Name
colorScheme: both
tokens:
primaryColor: "#7c3aed"
backgroundColor: "#111111"
widgetBackground: "#222222"
widgetBorderRadius: 12px
fontFamily: Inter
textColor: "#ffffff"
modeOverrides:
light:
backgroundColor: "#ffffff"
widgetBackground: "#f3f4f6"
textColor: "#111827"
---

```html
<canvas id="c"></canvas>
```

```css
body { margin: 0; }
canvas { display: block; }
```

```js
/* your background animation code */
```

Basic Themes omit the fenced blocks entirely — only the frontmatter is needed. Dynamic Themes must include all three.

YAML quoting tip. Hex colors (#7c3aed) and version numbers must be quoted. Unquoted # starts a comment in YAML.

Field Reference

All metadata lives in the YAML frontmatter:

FieldTypeRequiredDescription
monodit"theme"YesDiscriminator literal. Must be theme.
specVersion"3"YesManifest spec version. Must be "3".
namestringYesTheme display name.
versionstringYesTheme version string.
themeType`"basic""dynamic"`No
descriptionstringNoShort description.
authorstringNoAuthor name or handle.
colorScheme`"light""dark""both"`
tokensobjectYesBase token object. Includes required core token keys.
modeOverridesobjectNoOptional per-mode token overrides (light / dark).
dynamic (html/css/js blocks)html/css/js fenced blocksNoDynamic source blocks (html, css, js) in markdown fences.

For Dynamic Themes, the body holds three fenced code blocks (html, css, js) for the background-layer source. Basic Themes omit the body entirely.

For import/update decisions, monodit treats a theme revision as the canonicalized manifest identified by:

  • manifestId
  • version
  • contentHash

contentHash is computed from the normalized manifest body, so formatting-only edits do not create a new revision.

Basic vs Dynamic Themes

  • A Basic Theme sets "themeType": "basic" and must omit the dynamic block.
  • A Dynamic Theme sets "themeType": "dynamic" and must include the dynamic block.

Imports are validated against that rule. If themeType and the manifest body disagree, the import is rejected.


Basic Theme: Design Tokens

The tokens object defines the visual foundation of the dashboard. All six fields are required.

TokenCSS VariableDescriptionExample
primaryColor--primary-colorAccent color for buttons, links, highlights"#7c3aed"
backgroundColor--background-colorDashboard background"#111111"
widgetBackground--widget-backgroundWidget card background"#222222"
widgetBorderRadius--widget-border-radiusWidget card corner radius"12px"
fontFamily--font-familyFont stack for all text"Inter, sans-serif"
textColor--text-colorPrimary text color"#ffffff"

How Tokens Become CSS Variables

On dashboard load, monodit calculates the final tokens based on the current mode:

  1. Start with the values in the tokens (base) object.
  2. If modeOverrides[currentMode] exists, merge those values on top of the base tokens.
  3. Convert each camelCase token key to a --kebab-case CSS variable and inject it at :root.

For example, if backgroundColor is "#111111" in tokens but "#ffffff" in modeOverrides.light, the dashboard will use --background-color: #ffffff when in Light Mode.

Custom Tokens

tokens:
primaryColor: "#e11d48"
backgroundColor: "#fdf2f8"
widgetBackground: "#ffffff"
widgetBorderRadius: 20px
fontFamily: "Georgia, serif"
textColor: "#1e1b4b"
accentSecondary: "#7c3aed"
borderColor: "rgba(0,0,0,0.08)"

Custom keys like accentSecondary become --accent-secondary and borderColor becomes --border-color.


Dynamic Theme: The Background Sandbox

A Dynamic Theme runs in a sandboxed iframe positioned behind the entire dashboard grid (z-index: -1, pointer-events: none by default). It has access to the same Bridge API (ctx) as widget sandboxes.

Use Cases

  • Animated gradient or particle background
  • WebGL / Canvas shader
  • Time-of-day color shifts
  • Anything that requires live JavaScript

The Sandbox Environment

Dynamic Themes run in the same security context as custom widgets:

  • <iframe sandbox="allow-scripts"> — no allow-same-origin
  • No access to window.parent properties (cross-origin isolation)
  • No localStorage, no cookies
  • No import/export (ES modules blocked)
  • window.ctx bridge is injected before your JS runs

See the Widget Authoring Guide for the full constraints table.

Available ctx APIs in Dynamic Themes

All standard Bridge API methods are available:

  • ctx.instanceId — unique ID for this dynamic theme instance
  • ctx.config — persistent config for the dynamic theme
  • ctx.storage — private KV store
  • ctx.store — shared cross-widget KV store
  • ctx.assets — bucket-aware asset storage
  • ctx.proxyFetch(url) — server-side proxied fetch (requires login)
  • ctx.openUrl(url) — open a URL in the host window
  • ctx.onResize(cb) — fires on iframe resize
  • ctx.onConfigChange(cb) — fires when config is updated
  • ctx.onTokensChange(cb) — fires when the dashboard tokens change
  • ctx.setGlobalTokens(tokens) — programmatically override dashboard CSS variables

Pro Feature: ctx.setGlobalTokens allows Dynamic Themes to push new token values (like colors) to the host dashboard. The changes are reflected immediately across the entire UI and all active widgets.

Storage Guidance for Dynamic Themes

Dynamic Themes can use the same storage APIs as widgets:

  • ctx.storage for tiny private state
  • ctx.store for shared JSON documents
  • ctx.assets for files in R2

However, there is one important difference:

  • widget manifests can declare storage contracts that power host-managed bucket compatibility UI
  • dynamic theme manifests do not currently expose that same host-managed storage contract layer

So for Dynamic Themes:

  • explicit namespaces and bucket IDs are fine
  • stable naming matters
  • you should document the expected structure if the theme needs shared state

Examples:

// Shared document used by a dynamic theme
await ctx.store.set({ palette: "sunset" }, "theme-palette");

// Asset bucket usage
const assets = await ctx.assets.list();
if (assets[0]) {
console.log("Background asset:", assets[0].url);
}

For full API details, see Bridge API Reference and Storage & Buckets.


Full Working Examples

Example 1 — Minimal Dark Theme (Basic Only)

A clean, high-contrast dark theme with a purple accent.

---
monodit: theme
specVersion: "3"
themeType: basic
name: Obsidian
description: High-contrast dark with violet accent
version: "1.0.0"
colorScheme: dark
tokens:
primaryColor: "#8b5cf6"
backgroundColor: "#0f0f0f"
widgetBackground: "#1a1a1a"
widgetBorderRadius: 8px
fontFamily: "JetBrains Mono, monospace"
textColor: "#f5f5f5"
---

Example 2 — Warm Light Theme (Basic Only)

A warm, paper-like light theme.

---
monodit: theme
specVersion: "3"
themeType: basic
name: Parchment
description: Warm paper tones for easy reading
version: "1.0.0"
colorScheme: light
tokens:
primaryColor: "#b45309"
backgroundColor: "#fdf6e3"
widgetBackground: "#fffbf0"
widgetBorderRadius: 12px
fontFamily: "Georgia, serif"
textColor: "#292524"
---

Example 3 — Animated Gradient Background (Basic + Dynamic)

Pairs a dark basic theme with a slow-moving gradient background animation.

---
monodit: theme
specVersion: "3"
themeType: dynamic
name: Aurora
description: Animated aurora gradient behind the dashboard
version: "1.0.0"
colorScheme: dark
tokens:
primaryColor: "#22d3ee"
backgroundColor: "#03071e"
widgetBackground: "rgba(10, 10, 30, 0.85)"
widgetBorderRadius: 16px
fontFamily: "Inter, sans-serif"
textColor: "#e0f2fe"
---

```html
<canvas id="c"></canvas>
```

```css
* { margin: 0; padding: 0; }
body { overflow: hidden; width: 100vw; height: 100vh; }
canvas { display: block; width: 100%; height: 100%; }
```

```js
const c = document.getElementById('c');
const gl = c.getContext('2d');
let t = 0;

function resize() {
c.width = window.innerWidth;
c.height = window.innerHeight;
}
resize();
ctx.onResize(resize);

function draw() {
t += 0.003;
const w = c.width;
const h = c.height;
const g = gl.createLinearGradient(
w * Math.sin(t) * 0.5 + w * 0.5, 0,
w * Math.cos(t * 0.7) * 0.5 + w * 0.5, h,
);
g.addColorStop(0, `hsla(${200 + Math.sin(t) * 30},80%,15%,1)`);
g.addColorStop(0.5, `hsla(${260 + Math.cos(t * 0.8) * 40},70%,10%,1)`);
g.addColorStop(1, `hsla(${180 + Math.sin(t * 1.2) * 20},60%,8%,1)`);
gl.fillStyle = g;
gl.fillRect(0, 0, w, h);
requestAnimationFrame(draw);
}
draw();
```

Widget transparency tip: For Dynamic Themes to show through widget cards, set widgetBackground to an rgba(...) value with an alpha less than 1, as shown above.


Example 4 — Time-of-Day Theme (Basic + Dynamic)

Switches color tokens based on the local hour using ctx.setGlobalTokens.

---
monodit: theme
specVersion: "3"
themeType: dynamic
name: Circadian
description: Warm days, cool evenings, dark nights
version: "1.0.0"
colorScheme: both
tokens:
primaryColor: "#f59e0b"
backgroundColor: "#fefce8"
widgetBackground: "#ffffff"
widgetBorderRadius: 12px
fontFamily: "Inter, sans-serif"
textColor: "#1c1917"
---

```html
```

```css
```

```js
function getScheme(hour) {
if (hour >= 6 && hour < 12) {
return { backgroundColor: '#fefce8', widgetBackground: '#ffffff', textColor: '#1c1917', primaryColor: '#f59e0b' };
}
if (hour >= 12 && hour < 18) {
return { backgroundColor: '#fff7ed', widgetBackground: '#fffbf5', textColor: '#1c1917', primaryColor: '#ea580c' };
}
if (hour >= 18 && hour < 21) {
return { backgroundColor: '#1e1b4b', widgetBackground: '#2e2b5b', textColor: '#e0e7ff', primaryColor: '#818cf8' };
}
return { backgroundColor: '#030712', widgetBackground: '#111827', textColor: '#f9fafb', primaryColor: '#6366f1' };
}

function apply() {
ctx.setGlobalTokens(getScheme(new Date().getHours()));
}

apply();
setInterval(apply, 60_000);
```

Empty fenced blocks are still required for Dynamic Themes. Even if your theme only ships JS (no markup or CSS), include empty html and css fences to make the file structurally valid.


Example 5 — Dual-Mode Theme (Basic Only)

A single theme file that supports both Light and Dark modes using modeOverrides.

---
monodit: theme
specVersion: "3"
themeType: basic
name: Slate & Snow
description: Adaptive theme with high-contrast modes
version: "1.0.0"
colorScheme: both
tokens:
primaryColor: "#3b82f6"
backgroundColor: "#0f172a"
widgetBackground: "#1e293b"
widgetBorderRadius: 10px
fontFamily: "Inter, sans-serif"
textColor: "#f8fafc"
modeOverrides:
light:
backgroundColor: "#ffffff"
widgetBackground: "#f1f5f9"
textColor: "#0f172a"
---

Tip: When using Dynamic Themes, you can use ctx.onTokensChange to react when the user manually toggles Light/Dark mode, allowing your background animation to adjust its palette accordingly.


Import & Export

Importing a Theme

  1. Open the dashboard sidebar (edit mode).
  2. Go to the Theme tab → Added section.
  3. Click Import theme from file and select a .monodit-theme.md file.

The theme is applied immediately. themeType is validated during import, and Dynamic Themes activate their dynamic block right away.

Import behavior is revision-aware:

  • If the exact same theme revision already exists in Core, import is skipped.
  • If the exact same theme revision already exists in Added, import is skipped.
  • If Core contains the same manifestId but a different revision, the import is allowed as a new Added theme.
  • If Added contains the same manifestId but a different revision, monodit asks for confirmation and then overwrites the existing Added asset.

When an Added theme is overwritten, every dashboard that references that imported theme is updated to the new revision, and the imported theme is applied to the current dashboard immediately.

Exporting the Current Theme

Themes can be exported from either place:

  1. Open the dashboard sidebar → Theme tab and use the card menu on a Core/Added theme.
  2. Or use Export current theme from the account Export & Import section.

The exported file contains themeType, the tokens object, and the dynamic source (if active). Both official and imported themes export as .monodit-theme.md manifests.

File Naming Convention

Exported files use the pattern <slugified-name>.monodit-theme.md (e.g., aurora.monodit-theme.md).


Tips and Constraints

  • All six tokens are required in the tokens object. Missing any of the six standard tokens will cause rendering issues. Include sensible fallbacks even for tokens your theme does not actively customize.
  • Use modeOverrides for better accessibility. Instead of creating separate theme files for light and dark modes, use colorScheme: "both" and define modeOverrides to ensure your theme is readable in any environment.
  • Test both light and dark mode. Users can switch modes at any time. If your theme only supports one mode, set colorScheme to either "light" or "dark" to prevent users from switching to an unsupported mode.
  • Dynamic Themes are Pro-only. The server rejects any save that includes a dynamic block for non-Pro users.
  • No ES modules in Dynamic Theme JS. Same sandbox constraints as widgets — inline all dependencies.
  • rgba() tokens work. The CSS variable system passes token values verbatim, so semi-transparent backgrounds are supported.
  • fontFamily accepts any valid CSS font stack. If you reference a Google Font or web font, the host page must have the font loaded separately — the Dynamic Theme sandbox cannot load external fonts on behalf of the whole dashboard.