Skip to content

Ship your theme

A theme is finished when it defines every color the engine can ask for, clears both contrast floors on both canvases, and looks right when you render a real deck and look at it. This page is that list.

The tour covered about twenty colors. A shipping theme defines 98.

Skipping a color has a cost, and it comes in two sizes. Most omitted tokens fall back to an engine default, so a theme that stops at the surfaces, the ink and the accent renders its code blocks and its diagrams in somebody else’s colors, quietly. It will look almost right, which is worse than looking wrong.

The twelve categorical pairs are the exception: there is no engine default for them. Omit them and they resolve to nothing, so a chart or a flowchart draws every category in the same surface color with unreadable labels. That is the one skipped group that fails loudly rather than quietly.

Beyond the groups on the token tour, define:

Also requiredCountNotes
--hljs-* code syntax colors12each readable on --code-bg
--cat-N-fill / --cat-N-mark12 pairsthe categorical cycle
--cat-N-ink12generated, not hand-picked
--chart-cat1…8, --chart-state-*13the chart family’s own palette
--diagram-*severalstroke, line, lifecycle, alarm
--scheme-dark-*10the dark inputs your pairs read
--pass-bg / --warn-bg / --fail-bg3tinted signal grounds
--seq-5001anchors a ten-step gradient ramp
--text-label, --code-text2the accent-hued label tier, and code text
--c-container/-edge, --c-subcontainer/-edge, --c-on-container, --c-on-subcontainer6the diagram grouping boxes — see below

The exact list is the CONTRACT array in test/unit/palette/token-parity.test.js. The twelve --cat-N-ink values sit outside it and have their own gate.

--seq-500 is the middle of a ten-step ramp: the engine builds nine more shades around it, some closer to the page color, some further from it. Both halves of its light/dark pair have to sit mid-range on their own canvas. Put the anchor near white or near black and the nine have nowhere to go — they bunch up and stop looking like separate steps, even though each one is still readable on its own. Check where the steps land, not where the anchor sits:

Terminal window
node tools/composed-contrast.js evergreen

--on-accent you pick yourself, by eye, for both canvases. Nothing computes it for you. Do not fade it to a quieter shade for small text either: you tuned it to clear the contrast floor with nothing to spare, and fading it spends exactly that.

The six containment tokens are the boxes a diagram draws around related nodes — two fills, two edges, two label inks. They look skippable and are not: the fill is deliberately a barely-there step from the canvas, so a theme that omits them gets an invisible box with unreadable labels inside it.

  • /* @theme <name> */ matches the filename and the manifest’s name.
  • The file declares no @size — the page box belongs to the engine.
  • All eleven core tokens set directly: --bg, --bg-alt, --border, --text-heading, --text-body, --text-secondary, --text-muted, --muted-mark, --accent, --accent-soft, --surface-inverse.
  • Every surface, ink and accent token is a light-dark() pair.
  • --cat-on-fill and --cat-on-mark flip with the canvas.
  • The categorical three-layer contract holds in both canvases.
  • node tools/derive-cat-ink.js run and its output committed.
  • --seq-500’s dark half re-anchored mid-range, checked with composed-contrast.
  • <name>-dark.css is the wrapper and nothing more — the @theme line, @import, and :root { color-scheme: dark; }.
  • All 98 tokens defined directly, not inherited.
  • themes/<name>.manifest.json declares role: "base" — that is what puts the palette in the token-contract suite’s scope. npm run new:theme writes it for you; a theme without it is never tested.
  • The theme added to .vscode/settings.json under markdown.marp.themes, if you preview decks in VS Code. Nothing checks this; it only affects that editor’s preview.
  • swatch in the manifest replaced — the scaffold stamps #FF00FF, and nothing fails if you leave it.
  • npm run build run once, so the generated theme catalog and the docs site’s landing tokens pick up the new palette. build:check fails until you do.
  • node tools/bless-palette-baselines.js run and its diff committed (--dry-run first, to see what it would write) — this adds your palette’s frozen contrast and color-blindness measurements. Until it exists, the palette suite has nothing to compare your theme against and reports it as new.
  • npm run test:palette green.
  • npm run build:check green.
  • The component gallery rendered in both canvases and looked at.
Terminal window
npm run new:theme evergreen # scaffold
node tools/derive-cat-ink.js # generate the twelve categorical inks
node tools/contrast-audit.js # every text token vs its surface
node tools/cvd-audit.js # how it reads to color-blind viewers
node tools/composed-contrast.js evergreen
npm run test:palette # the palette suite
npm run build:check # the blocking gates
node lattice-emulator.js test/integration/baseline-decks/gallery.md \
/tmp/gallery.pdf -p evergreen # render the gallery and LOOK at it

Every item above is checkable, which means every item above can pass while the palette is merely acceptable. The gap between “clears the floors” and “reads like a designed document” is taste, and the only way through it is to render a real deck, look at every page, and fix what falls short.

  • Build the layouts to go with it: Component anatomy.
  • Add atmosphere behind the words: Finish anatomy.
  • Pick colors visually instead: the Studio derives a full theme from ten choices and reports every contrast pair as you go.