Skip to main content

Writing algorithms

Author your own algorithm

An algorithm is the durable, reusable asset behind every theme: a named recipe that maps a few anchor colors and knobs into a full, internally consistent token register. It is a xript (opens in a new tab) mod, so the toolchain, the capability model, and the zero-authority sandbox come free. This page covers what a mod looks like, the two ways to write one, and how to prove it before it ships.

What an algorithm is

The engine (@xoji/core) is mechanism: the open token register, the dependency graph, the contrast math, the coverage contract, emit. It holds no opinion about how a ramp should look. The algorithm is the opinion: how this one derives. A theme is just one materialized invocation of an algorithm, so the algorithm is the part worth writing once and reusing.

Mechanism stays in the engine
If a derivation choice would only ever make sense for one algorithm, it belongs in the algorithm, not in @xoji/core. That split is what lets many algorithms share one engine.

The built-in set

Five algorithms ship blessed. They are ordinary mods, the same shape yours takes, and a good place to read taste off working examples.

idposture
xoji-defaultthe neutral default; the shared taste with nothing turned up
xoji-quietrestrained; muted chroma, soft elevation
xoji-loudvibrant; boosted chroma and punchier depth
xoji-hchigh contrast; an AAA floor for accessibility
nxi-niteDay/Night; a time-of-day pass layered on the base

Anatomy of a mod

A mod is a directory: a manifest that declares it, and the code it runs. The manifest names the mod, the color-math capability that unlocks the cuti color primitives, and the script entry. The four exports the host calls (graph, traced, manifest, invariants) are registered for you by the authoring helper, so you never touch the raw plumbing.

algorithms/sunrise/
├─ mod-manifest.json   # declares the mod: name, capabilities, entry
└─ src/
   ├─ preset.ts        # the taste, as pure data
   └─ mod.ts           # defineXojiAlgorithm(spec)
{
  "$schema": "https://xript.dev/schema/mod-manifest/v0.7.json",
  "xript": "0.7",
  "name": "sunrise",
  "version": "0.1.0",
  "title": "Sunrise",
  "family": "xoji",
  "capabilities": ["color-math"],
  "entry": { "script": "src/mod.js", "format": "script" }
}

Tier 1: a family preset

The fast path. defineXojiAlgorithm takes a taste vector and runs the shared xoji derivation for everything you do not override. xoji-default's entire definition is { id: "xoji-default" }; from there you turn knobs up or down.

// src/mod.ts
import { defineXojiAlgorithm } from "@xoji/core/authoring";

defineXojiAlgorithm({
  id: "sunrise",
  anchors: { bg: "#1a1410", accent: "#ff9e5e" },
  vibrancy: 0.7,
  chroma: { accent: 1.4, palette: 1.3 },
});

Every field is optional but id; the rest fall through to the shared defaults.

fieldtypewhat it sets
idstringthe algorithm's id, how it loads and derives
anchors{ bg, fg, accent }baked default anchors; any omitted one derives from the others
vibrancy0–1how saturated the derivation runs before the knob overrides it
chroma{ accent, status, palette, neutral, accentTint }per-family chroma multipliers over the shared derivation
contrast{ floor, textOnFill }the AA/AAA floors the derivation must clear
elevation{ strength, alphaBoost }shadow weight and overlay opacity
knobsstring[]the knobs this algorithm accepts (defaults to the shared set)
passes(preset, input) => Pass[]ordered passes for a staged derivation (single-pass if omitted)

Tier 2: from scratch

When you want to own the derivation, defineAlgorithm hands you the input and the cuti color primitives and asks for the token graph back: an array of nodes, each naming a token, its value, and the tokens it derives from. Declare what you produce and how it categorizes so the coverage contract and emitters know the shape.

import { defineAlgorithm } from "@xoji/core/authoring";

defineAlgorithm({
  id: "mono",
  produces: ["--bg-0", "--fg-0", "--accent", "--accent-fg"],
  categories: { color: ["--bg-0", "--fg-0", "--accent", "--accent-fg"] },
  knobs: ["scheme"],
  derive(input, { cuti }) {
    const bg = input.anchors?.bg ?? "#101216";
    const accent = cuti.desaturate(input.anchors?.accent ?? "#8a8f98", 0.4);
    const inks = ["#ffffff", "#0a0a0a"];
    return [
      { name: "--bg-0", value: cuti.toHex(bg) },
      { name: "--fg-0", value: cuti.pickReadable(bg, inks), refs: ["--bg-0"] },
      { name: "--accent", value: cuti.toHex(accent) },
      { name: "--accent-fg", value: cuti.pickReadable(accent, inks), refs: ["--accent"] },
    ];
  },
});
One color implementation, shared
cuti (toOklch, mix, pickReadable, contrast, and the rest) is the same math the engine derives with, so a token your mod produces can't drift from one the engine produces.

Deriving in passes

An algorithm that derives in stages lists ordered passes. Each is a register → register transform; the first receives the anchors and knobs, and later ones reshape what came before. A family algorithm typically starts from settlePass (the shared derivation) and layers on top. nxi-nite is the worked example, shifting the whole palette by time of day after the base settles.

import { defineXojiAlgorithm, settlePass } from "@xoji/core/authoring";

defineXojiAlgorithm({
  id: "day-cycle",
  passes: (preset, input) => [
    settlePass(preset, input),   // the shared xoji derivation
    timeOfDayShift(input),       // a register → register transform
  ],
});

Prove it

The gauntlet runs your algorithm against extreme and random anchor sets and checks every invariant it declares: contrast floors held, no token collapsed, nothing parsed to NaN. Run it hosted to prove the sandboxed mod that actually ships. The coverage check confirms a component's consumed tokens are all in what you produce.

# prove the invariants across extremes + random anchor sets
npx xoji gauntlet -a sunrise --mode hosted --depth full

# confirm a component's consumed tokens are all produced
npx xoji coverage -a sunrise --consumed --bg-0,--fg-0,--accent

The gauntlet proves a theme is safe, not that it is good. For that, derive a real theme and look at it under the components on The Bench .

Load it

Build the mods to bundle your mod.ts into the self-contained mod.js the host runs, and the algorithm loads by id, the same path the built-ins take. From there it works everywhere the engine does: the CLI, the importable API, and the browser.

npm run build:mods            # bundles src/mod.ts → src/mod.js
npx xoji derive -a sunrise    # loads by id, derives a theme

Questions or contributions live on GitHub (opens in a new tab) .