Themes
A XenolithTheme bundles three things:
interface XenolithTheme { id: string tokens: XenTokens // design tokens paletteStyle?: PaletteStyle // DOM styling for palette / menus / overlay needsBackdrop?: boolean // opt into per-frame backdrop RT renderNode?: (node, opts, ctx) => NodeView // custom node rendering renderReroute?: (node, opts, ctx) => NodeView // custom inline reroute-knot rendering renderRerouteNode?: (node, opts, ctx) => NodeView // custom pullable Reroute-node rendering drawEdge?: (g, from, to, opts) => Graphics // custom wire rendering createGrid?: () => Container // custom canvas backdrop}Each hook is optional. When omitted, the editor falls back to Xen’s built-in renderer for that element — so a “theme” can be anything from a 5-line palette tweak to a fully custom Mesh + GLSL material.
renderReroute styles the inline $reroute knot (a wire passing through a dot); renderRerouteNode styles the pullable rectangular Reroute node. Liquid Glass overrides both with glass discs/boxes tinted by the wire’s pin type. paletteStyle drives the DOM chrome — the insert palette, the edge context menu, and the “Rendering…” busy overlay — so they all restyle with the active theme.
Built-in themes
Two themes ship in the box.
Xen — default
import { XenolithEditor } from '@xenolithengine/graph-editor'
const editor = await XenolithEditor.init('#app')// Xen is the default — no `theme` option needed.Original dark/gold design system: #0F110E node bodies, gold accents, category-tinted headers, typed-pin colour-coded wires, animated collapse to pill. Lives in @xenolithengine/graph-theme-xen.
Liquid Glass — frosted glass inspired by new Apple design
import { XenolithEditor } from '@xenolithengine/graph-editor'import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
const editor = await XenolithEditor.init('#app', { theme: liquidGlassTheme })Custom PIXI v8 Mesh + GLSL material. Each frame the editor renders the world (minus the nodes layer) into a backdrop RenderTexture; every glass body samples that texture through its shader with edge-localised refraction, gaussian blur, and a vertical tint. Sits over a radial-gradient navy canvas with a soft dot grid.
The backdrop pass is opt-in: Liquid Glass sets needsBackdrop: true, Xen does not — so swapping back to Xen turns the extra render pass off entirely.
Runtime theme switching
import { xenTheme } from '@xenolithengine/graph-render-pixi'import { liquidGlassTheme } from '@xenolithengine/graph-theme-liquid-glass'
editor.setTheme(liquidGlassTheme) // every node re-rendered through the new themeeditor.setTheme(xenTheme) // selection, hover, collapsed state, positions preservedsetTheme accepts either a full XenolithTheme or a DeepPartial<XenTokens> token override (which gets deep-merged into the active theme’s tokens).
Token overrides
If you just want to tweak Xen’s palette, pass an override in init:
const editor = await XenolithEditor.init('#app', { theme: { category: { logic: { accent: '#3FB8FF' }, data: { accent: '#FF66C4' }, macro: { accent: '#FFB347' }, utility: { accent: '#A5F3C5' }, }, pinType: { float: { color: '#3FB8FF', edgeColor: '#3FB8FF' }, object: { color: '#FF66C4', edgeColor: '#FF66C4' }, string: { color: '#FFB347', edgeColor: '#FFB347' }, }, },})Every other token (geometry, typography, effects, surfaces) stays at the Xen defaults.
Category colours in the graph data (data-first)
Token overrides change a category colour in code, which can’t travel inside a saved graph. To colour your own categories (agent, warehouse, …) — or a single node — straight from the xenolith.v1 data, use the optional categories palette and per-node render.color. Both round-trip through editor.toJSON() / loadJSON().
editor.loadJSON({ version: 'xenolith.v1', // graph-owned palette: a solid accent, or an explicit header gradient categories: { agent: { color: '#7C5CFF' }, warehouse: { gradient: { start: '#1FB6A8', end: '#0E5C55' } }, }, nodes: [ { id: 'a1', type: 'Planner', position: { x: 0, y: 0 }, pins: [], render: { category: 'agent', title: 'Planner' } }, // per-node colour beats the category { id: 'a2', type: 'Boss', position: { x: 240, y: 0 }, pins: [], render: { category: 'agent', title: 'Boss', color: '#FF3366' } }, ], edges: [],})Resolution order for a node’s header colour: render.color → categories[category] → theme category token → utility fallback. Both fields are optional — graphs without them render exactly as before, using the theme’s built-in categories. Types CategoryColorSpec / GraphCategoryPalette are exported from @xenolithengine/graph-render-pixi.
Use token overrides to restyle the whole visual language; use the categories palette to colour domain categories that live in your data.
The full token shape
The full type lives in @xenolithengine/graph-theme-xen and is XenTokens. Top-level groups:
| Group | Purpose |
|---|---|
color.brand | Brand / hover / accent yellows and their alphas. |
color.surface | Canvas, node body, panel, divider, header-end gradient stop. |
color.text | Primary, secondary, muted, disabled text colours. |
pinType | One entry per pin type: color, edgeColor, shape, edgeWidth. |
category | Header accent + Figma gradient per category (logic, data, macro, utility). |
pill | Pill-form accent gradient CSS strings (green, orange, …). |
state | hover, selected, active, disabled — border + glow tokens. |
geometry | Node, pin, header, edge, comment, reroute (radius, ringWidth) and edge midpointRadius dimensions. |
typography | Inter family + heading / label / comment styles. |
effect | Header inner shadow, backdrop blur, rim highlight, drop shadow. |
background | Canvas colour and dot-grid configuration. |
Building your own theme
For a palette-only theme, an object literal is enough:
import type { XenolithTheme } from '@xenolithengine/graph-render-pixi'import { xenTokens } from '@xenolithengine/graph-theme-xen'import { mergeTheme } from '@xenolithengine/graph-theme-xen'
export const pastelTheme: XenolithTheme = { id: 'pastel', tokens: mergeTheme(xenTokens, { color: { surface: { canvas: '#FAF7F2', node: '#FFFFFF' }, text: { primary: '#1A1A1A', secondary: '#666666' }, }, background: { color: '#FAF7F2', grid: { color: 'rgba(0,0,0,0.06)' } }, category: { logic: { accent: '#7FB8A1' }, data: { accent: '#7FA8C7' }, macro: { accent: '#C77F9F' }, utility: { accent: '#444444' }, }, }),}For a custom material (your own shader, your own backdrop), implement renderNode and optionally createGrid. See @xenolithengine/graph-theme-liquid-glass’s source for a complete worked example — PIXI Mesh + GLSL with SDF refraction and backdrop sampling.
Fonts
Themes declare what font families they need; the editor loads them. A theme exposes a
fonts: FontSpec[] field — FontSpec is { family: string; weights?: number[]; styles?: ('normal' | 'italic')[] }.
import type { XenolithTheme } from '@xenolithengine/graph-render-pixi'
export const myTheme: XenolithTheme = { id: 'mine', tokens: { /* ... */ }, fonts: [{ family: 'Inter', weights: [400, 600, 700] }], // ...}By default, the editor fetches the listed families from the Google Fonts CDN (fonts.gstatic.com). Zero shipped binaries, zero bundler config — works in any host out of the box.
Self-hosting (airgap, strict CSP, privacy)
Pass per-(family, weight, style?) URLs to override the CDN. Keys follow the pattern <family>|<weight> or <family>|<weight>|<style> (style defaults to normal):
const editor = await XenolithEditor.init('#app', { fontUrls: { 'Inter|400': '/static/Inter-Regular.woff2', 'Inter|600': '/static/Inter-SemiBold.woff2', 'Inter|700': '/static/Inter-Bold.woff2', },})Or at runtime (e.g. lazy-load after auth check):
editor.fonts.selfHost({ 'Inter|400': '/static/Inter-Regular.woff2', // ...})Anything not in the map still falls back to Google Fonts — so partial self-host is allowed (cover just the weights you have local). To download Inter WOFF2 binaries, grab them from the Inter Google Fonts page and host alongside your app assets.
Custom fonts (any family, not just Inter)
A theme can list any Google-Fonts-available family:
fonts: [ { family: 'Inter', weights: [400, 600, 700] }, { family: 'JetBrains Mono', weights: [500] }, { family: 'Orbitron', weights: [500, 700] },]The editor builds one Google Fonts API URL per call covering every requested family + weight, so even multi-family themes incur a single network round-trip.
If the family isn’t on Google Fonts (your own brand typeface), self-host every weight — the loader falls back to CDN only for families it has no URL for.