Skip to content
github-actions[bot] edited this page Oct 7, 2026 · 3 revisions

Themes

A theme pack is JSON: semantic colour tokens for light and/or dark mode, plus optional CSS and a startup splash. ZenKit applies the active pack to its own UI, to @nynxz/zenkit-ui components and (by default) to ComfyUI itself. Style your plugin from the --zen-* tokens and it follows every theme for free.

Using the theme in a plugin

.my-panel {
  background: var(--zen-surface);
  color: var(--zen-text);
  border: 1px solid var(--zen-border);
  border-radius: var(--zen-radius);
}
.my-panel .hint {
  color: var(--zen-muted);
  font-family: var(--zen-mono);
}
.my-panel .go {
  background: var(--zen-accent);
  color: var(--zen-accent-text);
}

React to switches:

const zen = await whenZen()
zen?.theme.onChange((pack) => console.log('theme', pack, zen.theme.currentMode()))

Component tokens (--zen-*)

What plugins and @nynxz/zenkit-ui read. window.ZenKit.theme.tokens lists them. They are derived from the pack's semantic tokens (right column), or from ComfyUI's own palette when the comfy pack is active.

Token Derived from
--zen-bg --background
--zen-surface --card
--zen-surface-2 --secondary, --muted
--zen-input, --zen-field-bg --input
--zen-text --foreground
--zen-muted --muted-foreground
--zen-border, --zen-surface-border, --zen-control-border --border
--zen-accent, --zen-control-hover-border, --zen-ghost-hover-text --primary
--zen-accent-text black or white, whichever reads best on the accent
--zen-radius, --zen-radius-surface --radius
--zen-chrome-bg, --zen-control-bg, --zen-control-hover-bg --card
--zen-ghost-bg --background
--zen-danger, --zen-danger-text --destructive
--zen-warn, --zen-warn-text --warning (default #d97706)
--zen-ok, --zen-ok-text --success (default #16a34a)
--zen-info, --zen-info-text --info, else the accent
--zen-mono --font-mono, else a system monospace stack

Pack tokens

The shadcn-style semantic set a pack defines. Missing ones fall back to neutral dark defaults, so a pack can set only a few.

--background --foreground --card --card-foreground --popover --popover-foreground --primary --primary-foreground --secondary --secondary-foreground --muted --muted-foreground --accent --accent-foreground --destructive --destructive-foreground --border --input --ring --radius

Optional extras that ZenKit reads: --warning, --success, --info, --font-mono, --comfy-menu-bg. Any other custom property in a pack (e.g. --sidebar-*, or your own) is written to the page too, so pack CSS can use it.

Theme API

window.ZenKit.theme Meaning
packs() Pack ids, 'comfy' (ComfyUI's own palette) first.
packLabel(id) Display name.
packModes(id) ['light'], ['dark'] or both.
current(), currentMode() Active pack id and mode.
setPack(id) Switch. Unknown ids are ignored; the mode is clamped to one the pack supports.
setMode(mode) 'light' or 'dark'.
onChange(cb) Called with the pack id after any pack or mode change. Returns unsubscribe.
registerPack(pack) Register a ThemePack; false if invalid. Re-registering the active id re-applies it.
tokens The --zen-* names above.

The choice persists in localStorage (zenkit.theme.pack, zenkit.theme.mode).

Writing a theme pack

Put a folder with a theme.json in either place, then reload the browser (no rebuild):

Folder Notes
custom_nodes/ComfyUI-ZenKit/themes/<id>/ Built-ins; replaced on plugin update.
ComfyUI/user/zenkit/themes/<id>/ Your drop-ins; survive updates. Same id overrides a built-in.

ComfyUI-ZenKit serves the merged list at GET /zenkit/themes.

{
  "id": "harbor",
  "name": "Harbor",
  "modes": ["dark"],
  "tokens": {
    "dark": {
      "--background": "#0f1720",
      "--foreground": "#dfe7ef",
      "--card": "#16212c",
      "--primary": "#4fb3bf",
      "--border": "#26394a",
      "--radius": "10px"
    }
  },
  "css": "harbor.css",
  "splash": { "preset": "hearts" }
}
Field Required Meaning
id yes Kebab-case, unique.
name yes Shown in pickers.
tokens.light, tokens.dark at least one "--token": "value" maps. Any CSS colour syntax works, oklch() included. Non-string values are dropped.
modes no Supported modes; derived from which token sets exist when omitted.
css no CSS text, or a filename / list of filenames in the theme folder (the server inlines them; only the basename is used). Injected only while the pack is active.
splash no Startup screen; see below.

Invalid files are skipped, never fatal.

Theme CSS and the theming contract

Pack CSS is injected after the tokens, so it can override them. Scope it to these stable hooks and use !important to beat ZenKit's scoped styles:

Selector Matches
html[data-zen-theme-pack="<id>"] Root while your pack is active (also on body).
.zenkit-panel A panel's root.
.zenkit-panel-header, .zenkit-panel-title, .zenkit-panel-icon, .zenkit-panel-button, .zenkit-panel-body Panel parts.
[data-zen-status="open|minimized|folded"] On .zenkit-panel.
[data-zen-active="true"] The frontmost panel.
[data-zen-docked="true"], [data-zen-dock-side="right|bottom"] Docked panels.
[data-zen-tiled] Panels tiled in a workspace.
[data-zen-frame="none"] Bare panels.
[data-zen-interacting="true"] The panel being dragged or resized.
.lg-node, .lg-node-header ComfyUI's node DOM (Vue nodes).
/* harbor.css */
html[data-zen-theme-pack='harbor'] .zenkit-panel-header {
  border-bottom: 2px solid var(--zen-accent) !important;
}

CSS is trusted local content: it can't run script, but url() can fetch.

Theme packs are trusted, code-like content. A pack's CSS restyles all of ComfyUI (it can hide or disguise any button, and url() can send requests anywhere), so install packs only from people you trust, as you would a custom node. ZenKit validates the JSON and sanitises the startup splash (below), but it does not sandbox pack CSS.

Startup splash

splash sets what ComfyUI shows while loading, from the next load (ComfyUI draws it before any extension runs, so ZenKit stores it in localStorage for the next boot).

Field Meaning
preset A built-in sequence. Currently "hearts".
html, css Your own markup, written into #splash-loader. Scope CSS under #splash-loader; var(--splash-accent) is the pack's primary colour. Takes precedence over preset.

Without splash, the pack still gets ComfyUI's logo drawn in its colours. Users can turn themed startup off in Zen Settings.

ComfyUI's boot script writes the stored splash with innerHTML before any extension runs, so a pack's own html and css are sanitised before they are stored (the built-in presets are stored as written):

  • html keeps basic markup (div, span, p, headings, lists, b/i/em/strong, …) and SVG shapes (svg, g, path, circle, ellipse, rect, line, polygon, text, gradients, use, clipPath, mask, pattern) with presentation attributes, class, id and style. Script, on* attributes, iframe/object/embed/form, img, SMIL animation and every URL except a #fragment are removed. Animate with CSS.
  • css goes through the browser's CSS parser: @import is dropped, and so is any declaration whose url() is not same-origin (or a #fragment), or that uses image-set() or escapes.
  • The pack's background, foreground and accent colours are dropped if they hold a remote url().

If nothing of the html survives, the pack's preset (if any) is used instead.

Registering a pack from a plugin

registerZenPlugin({
  id: 'myplugin',
  plugin: 'My Plugin',
  themes: [
    {
      id: 'myplugin-dusk',
      name: 'Dusk',
      tokens: { dark: { '--background': '#1b1626', '--primary': '#c792ea' } },
      css: '.zenkit-panel-header { letter-spacing: .02em !important; }',
    },
  ],
})

Low level: window.ZenKit.theme.registerPack(pack). Here css must be CSS text; filename refs are only resolved for packs on disk.

A registered pack joins the theme pickers (the Start menu's, Zen Settings' and the ComfyUI logo menu's) at once, even if they are already open.

Motion, pop-ups and media tokens

Token Default What it sets
--zen-dur-fast 0.12s Hover and colour changes in every component
--zen-dur 0.2s Things that move or open (sections folding, switches)
--zen-shadow-pop 0 10px 30px rgb(0 0 0 / 45%) The shadow under every pop-up (menus, popovers, dropdowns, pickers)
--zen-media-bg #0b0b0e Behind images and video (media picker, LoRA browser, mention thumbnails)
--zen-star #f5b301 Favourite stars
--zen-playhead #ff5a4f ZenTimeline's playhead and progress fills
--zen-marker #f5c542 ZenTimeline markers without a colour of their own

With the system's reduced-motion setting on, both durations are 0, so nothing in the kit animates.

Clone this wiki locally