Output (CSS / JSON / TS / Tailwind)
Four surfaces, distinct roles:
| Surface | Path | Format | Typical consumer |
|---|---|---|---|
| JSON | dist/json/ | DTCG Resolver + sets | Vue / Flutter / tooling |
| CSS | dist/css/<variant>.css | CSS custom properties | Generic web / Tailwind |
| TS | dist/ts/<variant>.d.ts | Typed literals | TypeScript app |
| Tailwind | dist/tailwind/*.mjs | Preset + merge extension | Tailwind config |
JSON is the interop surface (structured values). CSS and TS are
resolved surfaces (hex / rem strings).
dist/ layout
dist/
manifest.json # sha256 + configHash + gitCommit (audit trail)
css/
<brand>.<mode>.<surface>.<dimension>.css
ts/
<brand>.<mode>.<surface>.<dimension>.d.ts
json/
resolver.json # DTCG index
…sets… # base / color / spacing / font (reference-closed)
tailwind/ # with output.tailwind
preset.mjs # generated Tailwind preset (naming layer)
tw-merge.mjs # tailwind-merge extensionmanifest.json feeds apollion-tokens build --check (CI drift gate).
JSON — structured values
{
"color": {
"$type": "color",
"$value": {
"colorSpace": "oklch",
"components": [0.55, 0.18, 25],
"hex": "#c8102e"
}
},
"spacing": {
"$type": "dimension",
"$value": { "value": 1, "unit": "rem" }
}
}color— OKLch working space + sRGBhexfallback.alphaonly if< 1.dimension—{ value, unit }(typicallyrem).
CSS — custom properties
Per variant, resolved strings under the --apollion- prefix (--apollion-{group}-{path}):
:root {
--apollion-bg-primary: #e31837;
--apollion-bg-paper: #ffffff;
--apollion-text-primary: #0b0b0b;
--apollion-edge-border: rgba(11, 11, 11, 0.2);
--apollion-spacing-md: 1rem;
}With output: { cssModes: true } the build also emits one file per
brand × surface × dimension carrying both modes,
css/<brand>.<surface>.<dimension>.modes.css, scoped exactly the way the core
mode engine scopes them — so a React-less page flips with the same attribute
(getModeScript() / syncDocumentMode() from @apollion-dsi/core/themes/mode):
:root, html[data-apollion-mode="light"] { color-scheme: light; --apollion-bg-paper: #ffffff; … }
html[data-apollion-mode="dark"] { color-scheme: dark; --apollion-bg-paper: #181818; … }
@media (prefers-color-scheme: dark) { html:not([data-apollion-mode]) { … } }TypeScript — literals
declare const tokens: {
readonly color: {
readonly primary: '#e31837';
};
readonly spacing: {
readonly medium: '1rem';
};
};Documented tokens carry their $description/$deprecated metadata as JSDoc
blocks in the emitted .d.ts, so a deprecated token (e.g. bg.info) shows as
strikethrough with its migration hint right at the call site.
Tailwind — generated preset + tailwind-merge extension
With output: { tailwind: true }, the build emits dist/tailwind/preset.mjs
and dist/tailwind/tw-merge.mjs — the naming layer for Tailwind stacks.
The CSS surface already carries the values; the preset maps foundation token
names to Tailwind theme keys through var(--apollion-*) indirection, so one
preset serves every variant and theme switching stays a CSS-only concern:
// tailwind.config.js
import apollion from '@apollion-dsi/tokens/dist/tailwind/preset.mjs';
export default {
presets: [apollion],
// …
};
// → class="bg-bg-primary/50 p-md shadow-2 text-tx-main"// cn.ts — clsx + tailwind-merge aware of the generated keys
import { extendTailwindMerge } from 'tailwind-merge';
import { twMergeExtension } from '@apollion-dsi/tokens/dist/tailwind/tw-merge.mjs';
export const twMerge = extendTailwindMerge(twMergeExtension);Both modules are pure projections of the same IR as every other surface — generated, never hand-maintained, and differential-tested against the CSS renderer so preset keys cannot drift from the emitted vars.
Alpha modifier. Tailwind's /50 opacity form needs raw channel triplets,
so the flag also adds -rgb companions to dist/css/ for opaque colour
tokens (--apollion-bg-primary-rgb: 0 55 80), and the preset maps them as
rgb(var(--apollion-bg-primary-rgb) / <alpha-value>). Tokens that already
carry alpha are deliberately excluded from the modifier form — mapping them
through it would silently drop their baked-in alpha — and keep a plain var()
entry (noted as a comment in the emitted preset). Requires the variant's
dist/css/ file in scope; scope is foundation groups only (border.style has
no Tailwind theme scale and is unmapped).
Design.md — resolved-values AI context
With output: { designMd: true }, the build emits dist/design/<brand>.design.md:
one document per brand, covering every (mode × surface × dimension) variant.
YAML frontmatter carries the resolved values for machines (AI assistants in your
repo stop guessing palette values); the markdown body renders the same values as
per-variant tables for humans.
The division of labor: the package's llms.txt teaches the
rules (which token to reach for, layer boundaries); design.md supplies the
values those rules reference — generated from your config seeds, so it can
never drift from your shipped theme. Covered by the build manifest like every
other emission.
// apollion.config.mjs
export default {
brands: {
acme: {
/* seeds */
},
},
output: { css: true, designMd: true },
};
// → dist/design/acme.design.mdDeterminism
Same config + same (nodeVersion, platform) → byte-identical dist/.
The build writes to a tmpdir and does an atomic rename — a mid-build failure leaves
the previous dist/ intact.
See also
- Build CLI
- Brand examples
- Colors — OKLch derivation in core