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 full contract
Section titled “The full contract”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 required | Count | Notes |
|---|---|---|
--hljs-* code syntax colors | 12 | each readable on --code-bg |
--cat-N-fill / --cat-N-mark | 12 pairs | the categorical cycle |
--cat-N-ink | 12 | generated, not hand-picked |
--chart-cat1…8, --chart-state-* | 13 | the chart family’s own palette |
--diagram-* | several | stroke, line, lifecycle, alarm |
--scheme-dark-* | 10 | the dark inputs your pairs read |
--pass-bg / --warn-bg / --fail-bg | 3 | tinted signal grounds |
--seq-500 | 1 | anchors a ten-step gradient ramp |
--text-label, --code-text | 2 | the accent-hued label tier, and code text |
--c-container/-edge, --c-subcontainer/-edge, --c-on-container, --c-on-subcontainer | 6 | the 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.
Three worth reading twice
Section titled “Three worth reading twice”--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:
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.
The checklist
Section titled “The checklist”-
/* @theme <name> */matches the filename and the manifest’sname. - 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-filland--cat-on-markflip with the canvas. - The categorical three-layer contract holds in both canvases.
-
node tools/derive-cat-ink.jsrun and its output committed. -
--seq-500’s dark half re-anchored mid-range, checked withcomposed-contrast. -
<name>-dark.cssis the wrapper and nothing more — the@themeline,@import, and:root { color-scheme: dark; }. - All 98 tokens defined directly, not inherited.
-
themes/<name>.manifest.jsondeclaresrole: "base"— that is what puts the palette in the token-contract suite’s scope.npm run new:themewrites it for you; a theme without it is never tested. - The theme added to
.vscode/settings.jsonundermarkdown.marp.themes, if you preview decks in VS Code. Nothing checks this; it only affects that editor’s preview. -
swatchin the manifest replaced — the scaffold stamps#FF00FF, and nothing fails if you leave it. -
npm run buildrun once, so the generated theme catalog and the docs site’s landing tokens pick up the new palette.build:checkfails until you do. -
node tools/bless-palette-baselines.jsrun and its diff committed (--dry-runfirst, 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:palettegreen. -
npm run build:checkgreen. - The component gallery rendered in both canvases and looked at.
The commands
Section titled “The commands”npm run new:theme evergreen # scaffoldnode tools/derive-cat-ink.js # generate the twelve categorical inksnode tools/contrast-audit.js # every text token vs its surfacenode tools/cvd-audit.js # how it reads to color-blind viewersnode tools/composed-contrast.js evergreennpm run test:palette # the palette suitenpm 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 itThe step with no command
Section titled “The step with no command”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.
Where to go next
Section titled “Where to go next”- 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.