Docs
Output (CSS / JSON / TS / Tailwind)

Output (CSS / JSON / TS / Tailwind)

Four surfaces, distinct roles:

SurfacePathFormatTypical consumer
JSONdist/json/DTCG Resolver + setsVue / Flutter / tooling
CSSdist/css/<variant>.cssCSS custom propertiesGeneric web / Tailwind
TSdist/ts/<variant>.d.tsTyped literalsTypeScript app
Tailwinddist/tailwind/*.mjsPreset + merge extensionTailwind 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 extension

manifest.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 + sRGB hex fallback. alpha only if < 1.
  • dimension{ value, unit } (typically rem).

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.md

Determinism

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