@siax/tokens (0.1.2)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/tokens@0.1.2"@siax/tokens": "0.1.2"About this package
@siax/tokens
The single source of truth for SIAX design token values. Everything else
in DES1GN — @siax/ui's CSS, Tailwind utilities, future native targets —
should eventually be derived from this package, not the other way around.
Where the values actually come from
Canonical origin: sax3l/siax-io, app/globals.css (1866 lines, OKLCH,
heavily commented), vendored verbatim at
reference/siaxio-app-globals.css
@ commit 905b425b3a1260f8d8c1570769a8a5ce50b62ef4.
This corrects the provenance recorded in the first version of this package.
packages/ui/src/globals.css was cited as the source of truth; it is not —
it is a reduced derivation of the canonical sheet. Measured against the
canonical file, @siax/ui's 111-line sheet carries colour and radius only,
and silently omits:
Missing from @siax/ui/src/globals.css |
Count | Canonical location |
|---|---|---|
--sidebar-* |
8 × 2 themes | globals.css:73-81, :110-117 |
--elevation-1..5 |
5 × 2 themes | globals.css:1768-1791, :1797-1815 |
--depth-shadow, --depth-shadow-lifted, --lift-hard |
3 × 2 themes | globals.css:1611-1627, :1638-1648 |
--duration-* (6), --ease-* (5) |
11 | globals.css:1742-1755 |
--perspective, --spatial-ease, --spatial-duration |
3 | globals.css:1604-1632 |
--z-base/raised/dropdown/modal/toast |
5 | globals.css:1758-1762 |
data-universe accent system |
7 universes × 2 themes | globals.css:120-140 |
Fluid type scale (.fluid-*) |
6 steps | globals.css:210-240 |
Filmic layer (.filmic-*) |
6 utilities | globals.css:242-287 |
All of the above are now modelled here. The values @siax/ui did carry were
re-verified against the canonical source and every one matched byte-for-byte —
nothing had drifted, the sheet was merely incomplete.
Five colour tokens go the other way: success, success-foreground,
warning, warning-foreground and overlay exist in @siax/ui but not
in siax.io. They are kept (the admin consoles need status colours and a modal
scrim) and are flagged as INTENDED_ADDITIONS in build.mjs so the verify
pass reports them as deliberate rather than as drift.
What is DTCG, and why does this package use it
DTCG (Design Tokens Community Group) is the
W3C community group format for design tokens: plain JSON where every token is
an object with a $value and a $type key ({"$type": "color", "$value": "oklch(0.5 0.2 285)"}), and files use the .tokens.json extension. It went
stable in October 2025. Before this, every design-tokens tool (Figma Tokens
Studio, Style Dictionary, Specify, Superposition, …) invented its own JSON
shape, and syncing between them meant writing a converter. DTCG gives all of
them one shape to agree on.
Concretely, for SIAX this means:
- Figma sync becomes possible. The Tokens Studio plugin reads and writes
.tokens.jsonfiles directly. Point it atpackages/tokens/tokens/and a designer can push a token change straight into this repo as a PR, or pull this repo's tokens into Figma — no manual re-typing of hex/oklch values in either direction. - New output targets stop being a rewrite. Today the only consumer is
CSS custom properties. When
h0st/VehIQ need native tokens (iOSColor.xcassets/Android XML, or a FlutterThemeData), that's a new Style Dictionary platform pointed at the same.tokens.jsonsource — not a second hand-maintained token sheet that can drift from the CSS one. - Tokens become machine-checkable.
pnpm --filter @siax/tokens testrebuilds and diffs the output against the canonical siax-io sheet, so a typo'd value fails a script instead of shipping unnoticed.
Why Style Dictionary v4
Style Dictionary is the transform engine: it reads .tokens.json, resolves
references, and emits whatever target format you configure (CSS, JS, iOS,
Android, …). Version 4 is the release that treats DTCG ($value/$type) as
its native input format — earlier versions expected Style Dictionary's own
pre-DTCG shape (value/type, no $). Using v4 means the token source can
be pure DTCG with no Style-Dictionary-specific dialect mixed in, which is
what keeps it Tokens-Studio-compatible.
Layout
packages/tokens/
reference/
siaxio-app-globals.css VENDORED canonical source (sax3l/siax-io @ 905b425).
Read-only. `pnpm test` diffs against this.
tokens/
color/
light.tokens.json :root colours
dark.tokens.json .dark colours
sidebar/
light.tokens.json --sidebar-* (8), light
dark.tokens.json --sidebar-* (8), dark
elevation/
light.tokens.json --elevation-1..5, --depth-shadow*, --lift-hard, light
dark.tokens.json ditto, dark
universe/
README.md the data-universe system explained
<universe>/
light.tokens.json --accent, --ring
dark.tokens.json --accent, --accent-foreground, --ring
… one directory per named universe (7)
motion.tokens.json --duration-*, --ease-*, --perspective, --spatial-*
z-index.tokens.json --z-*
typography.tokens.json --font-sans/-mono + the fluid type scale
radius.tokens.json --radius (0rem, DESIGN_GUARD: stays 0)
marketing-only/
filmic.tokens.json NOT part of the platform set — see "Filmic" below
build.mjs Style Dictionary v4 build + verification
scripts/
validate-tokens.mjs structural lint (`pnpm lint`)
dist/ generated — do not hand-edit
css/variables.css platform layer: :root, .dark, 14 universe blocks
css/filmic.css marketing layer, separate on purpose
js/tokens.js, .d.ts typed objects: { light, dark, universes }
The universe accent system (data-universe, "Boken §2.3")
Eight universes exist. Seven are named and carry their own accent hue; the
eighth, core, renders no attribute at all and therefore inherits the
default SIAX purple. There is no universe/core/ directory by design — core
is the absence of an override.
| Universe | Hue (H) | Light --accent |
Dark --accent |
|---|---|---|---|
core (no attribute) |
285 | oklch(0.45 0.23 285) |
oklch(0.65 0.25 285) |
revops |
150 | oklch(0.45 0.12 150) |
oklch(0.70 0.14 150) |
dev |
240 | oklch(0.45 0.19 240) |
oklch(0.68 0.15 240) |
automation |
60 | oklch(0.47 0.11 60) |
oklch(0.75 0.14 60) |
effekt |
330 | oklch(0.46 0.20 330) |
oklch(0.68 0.21 330) |
optimering |
200 | oklch(0.45 0.09 200) |
oklch(0.72 0.12 200) |
bilhandlare |
30 | oklch(0.48 0.14 30) |
oklch(0.72 0.15 30) |
editorial |
270 | oklch(0.40 0.04 270) |
oklch(0.75 0.04 270) |
Light overrides --accent and --ring; dark additionally overrides
--accent-foreground. Nothing else — the @theme inline mapping cascades
the rest. The attribute goes on a route/template wrapper, never on an
individual component; the slug → universe mapping lives in siax-io's
lib/site-config.ts (universeForPath).
Because --lift-hard and --filmic-glow-shadow are built on
color-mix(… var(--accent) …), they re-tint per universe automatically. No
other shadow token does.
#open-questions
What is "Boken"? Every universe rule in the canonical CSS cites it
(app/globals.css:120: "Universum-accenter (Boken §2.3)"), and the citation
is load-bearing: it is what locks the hue and permits only lightness to be
revised. A repo-wide search of sax3l/siax-io @ 905b425 finds 29
references across 22 files, citing §1.5, §2.3, §4.4, §25, §27,
§33, §37, §45.1/A2, §50.3, kap 12/36 and kap 40 — i.e. a numbered
document of at least 50 chapters governing accent policy, section numbering,
CTA copy, FAQ patterns, newsletter components and the design-guard baseline
ratchet.
The document itself is not in the repository. find -iname '*boken*'
returns nothing; no .md contains it; and no reference is ever a link — every
citation is bare prose in a code comment. Its content is only ever visible
second-hand, through the rules quoted at those 29 sites.
Status: OKÄNT. It could be a Notion/Drive document, a printed brand book, or Simon's own working notes. Until it is located and archived, §2.3 cannot be re-derived — so if a universe hue is ever questioned, the CSS values in this package are the only surviving record of it. Recommendation: locate "Boken", commit a copy (or an authoritative extract) to DES1GN, and link it from here.
Font-stack divergence. @siax/config/tailwind-preset.css declares
--font-sans: var(--font-inter), 'Inter', ui-sans-serif, … (next/font wiring)
while the canonical source declares --font-sans: 'Inter', system-ui, -apple-system, sans-serif. Both are tracked; neither was changed here.
Reconciling them is a @siax/config decision, not a token-value question.
Filmic — why it is segregated, not deleted
tokens/marketing-only/filmic.tokens.json compiles to its own artifact,
dist/css/filmic.css, exported as @siax/tokens/filmic. It is never
concatenated into dist/css/variables.css, and --verify fails the build if
a filmic token ever appears in both.
The call: marketing-site-only. Evidence, all from siax-io @ 905b425:
- The layer's own spec (
docs/FILMIC-DESIGN-SYSTEM.md) states its purpose as "Låt video, ASCII och Simon bära det visuella" — a hero video, a WebGL/ASCII renderer, and the founder-presence system. An admin console has none of those. --filmic-overlay-gradientis literally a scrim for video.- Real adoption is one call site:
filmic-kickerincomponents/navigation/mega-menu.tsx:118. The other five utilities appear only in documentation. - The source header calls them "opt-in" (
app/globals.css:243) — never a baseline. - They are stylistic duplicates of the platform scale, not additions to
it:
.filmic-displayis a louder.fluid-title-hero,.filmic-leada more generous.fluid-body. Shipping both to an admin OS would give two competing display scales with no rule for choosing between them.
Recorded counter-argument: the kicker idiom (mono, uppercase, wide tracking)
genuinely is platform-wide — ContentHub eyebrows, section headings, card meta
labels. If that gets tokenised for the platform it should be re-derived under
a neutral name (eyebrow-*) in the main set, not by promoting this file;
these values are tuned against a full-bleed video backdrop.
How the build works
node build.mjs (via pnpm --filter @siax/tokens build, or
pnpm tokens:build from the repo root).
Style Dictionary resolves one merged token tree per build, and this system
reuses the same token names under many different selectors — --accent exists
in :root, in .dark, and in fourteen [data-universe] blocks with a
different value each time. Merging them would make later files silently
clobber earlier ones. So build.mjs runs Style Dictionary once per
selector (16 platform passes + 1 filmic pass), then stitches the blocks
together in the canonical cascade order. Same "build per theme, merge after"
pattern Style Dictionary's docs recommend for multi-brand setups, extended
from 2 passes to 17.
No colour-value transform is registered. Style Dictionary's built-in colour
transforms go through culori and can re-round or reformat oklch() strings;
this build needs byte-for-byte fidelity, so every $value is emitted exactly
as authored. The only transforms are on the name (the DTCG path,
kebab-joined) and two structural composers described below.
#spec-deviations
DTCG cannot losslessly express several things this design system actually
uses. Where that happens the literal CSS is the $value, and the reason
is recorded on the token. The complete list:
$type |
Form used | Why not the spec form |
|---|---|---|
color |
CSS string, "oklch(0.45 0.23 285)" |
See #why-string-color-values |
shadow |
CSS string | Multi-layer, plus inset, plus color-mix(in oklch, var(--accent) …). DTCG's shadow composite can express none of those and would silently drop layers. |
gradient |
CSS string | DTCG gradients are (color + position) stops with concrete colours; this one references var(--background) and color-mix(). |
duration |
CSS string, "0.25s" |
DTCG wants a (value + unit) object; string keeps byte fidelity. |
dimension |
CSS string | Needed for clamp(), em, ch, vw — none of which DTCG dimensions support. |
fontFamily |
CSS string (whole stack) | The source quotes some names and not others ('Inter', system-ui). Splitting to an array would require re-deriving quoting on output, which is not lossless. |
cubicBezier |
spec-legal 4-number array | The one composite kept in true DTCG form; build.mjs composes cubic-bezier(a, b, c, d). |
For the fluid type scale, DTCG has no representation for
clamp(min, Nvw + Mrem, max) at all. The literal is the $value, and the
decomposition (min / preferredVw / preferredRem / max) lives in
$extensions["io.siax.tokens"].fluid, which DTCG explicitly reserves for
vendor data. build.mjs recomposes the clamp() from those parts and fails
the build if it does not match the $value, so the documentation cannot
silently disagree with the value.
#why-string-color-values
The DTCG color spec also defines an object form for $value
({"colorSpace": "oklch", "components": [...], "alpha": ...}). This package
uses the plain CSS string form instead ("oklch(0.964 0.006 265)") for two
reasons: it round-trips through Style Dictionary with zero risk of
reformatting (see above), and Tokens Studio's own OKLCH support is newer and
less consistent than its plain-string "custom color" handling. If/when the
object form is broadly supported everywhere this package's output needs to
travel, migrating is a source-file change only — every consumer of dist/
is unaffected either way.
Verifying the output matches the source
pnpm --filter @siax/tokens test # node build.mjs --verify
The verifier does five independent things, and each one has been proven to fail on a deliberately injected fault:
- Custom properties, selector by selector. Both
reference/siaxio-app-globals.cssand the freshly builtdist/css/variables.cssare parsed into{ selector -> { property -> value } }maps and diffed. The parser is a brace-balanced walk, not a flat regex: the canonical sheet declares:rootthree separate times (lines 16, 1601, 1741) and.darkthree times (84, 1637, 1796), nests blocks inside@media/@layer/@supports/@keyframes, and uses attribute selectors — a naive/sel\s*\{([^}]*)\}/mis-reads all of it. Repeated selectors are merged the way the cascade merges them. - Font stacks are cross-checked against the canonical
@theme inlineblock, since that is where the source declares them. - The fluid scale is cross-checked against the literal
font-size/line-height/letter-spacinginside each.fluid-*class body — in both directions, so inventing aletter-spacingfor a step that has none upstream fails just as loudly as dropping one that does. - The filmic layer is cross-checked the same way against the
.filmic-*class bodies, and asserted to be absent from the platform artifact. - Cascade order is asserted. Values are only half the contract; which block wins is the other half.
Anything not on the INTENDED_ADDITIONS allowlist fails the build.
Current status: 137 canonical declarations across 16 selectors reproduced value-for-value; 0 unintended differences; 26 intended, allowlisted additions (5 DES1GN-only colours × 2 themes, 2 font stacks, 14 fluid-scale properties). Run the command above for the itemised list.
Adversarial checks performed on the verifier itself
A verifier that always passes is worse than none. These faults were injected and all were caught:
| Injected fault | Caught as |
|---|---|
Dark --accent L 0.65 → 0.66 |
VALUE MISMATCH |
editorial dark chroma 0.04 → 0.05 |
VALUE MISMATCH on the universe selector |
Fluid preferredVw 4 → 4.5 (desynced from its literal) |
build fails before emit |
--lift-hard deleted from the dark elevation set |
in canonical source but MISSING from build |
| A filmic token moved into the platform set | SEGREGATION BREACH (×2) + not-allowlisted |
| Pass order reversed | CASCADE ORDER differs |
Bug fixed in this pass
The previously committed dist/ was broken, in a way nothing tested for.
Both generated headers contained the glob **/*.tokens.json inside a /** … */
block comment. **/ closes the comment:
dist/js/tokens.jswas not valid JavaScript —node --checkreportsSyntaxError: Unexpected token '*', soimport '@siax/tokens'would have thrown at load.dist/css/variables.cssleaked comment prose into the stylesheet ahead of:root, a parse error at the top of the sheet.
Neither surfaced because no consumer imports dist/ yet. The headers no
longer emit */.
Adding or changing a token
- Edit the relevant
tokens/**/*.tokens.jsonfile — neverdist/, it's regenerated. Never editreference/— it is the upstream copy. pnpm --filter @siax/tokens lint— structural DTCG check.pnpm --filter @siax/tokens build.pnpm --filter @siax/tokens test— the five-part verification above.
Changing a value that exists upstream will (correctly) fail step 4: the
canonical source is siax-io, so the change belongs there first, followed by
re-vendoring reference/siaxio-app-globals.css and bumping
REFERENCE_COMMIT in build.mjs. That friction is the point.
Note on
$description: Style Dictionary v4 treats{…}inside any token string, including$description, as a token reference and hard-fails when it cannot resolve it. Do not write CSS snippets or object shapes with braces in prose — use parentheses.
Figma (Tokens Studio) — future wiring
Not wired up yet, but the source is already in the right shape for it. When this is picked up:
- Install the Tokens Studio for Figma plugin.
- Point its "Sync providers" at this repo (Gitea
git.siax.io/sax3l/DES1GN) using its Git/GitHub-compatible sync provider, file pathpackages/tokens/tokens/. - Tokens Studio reads each
*.tokens.jsonas a token set. Map them to Tokens Studio "themes":light→color/light+sidebar/light+elevation/light+radius+motion+z-index+typography;dark→color/dark+sidebar/dark+elevation/dark. Each universe becomes an additional enabled-on-top set, mirroring how[data-universe]overrides only--accent/--ringin CSS. - Designer edits in Figma become PRs against
.tokens.json— reviewed like any other code change, thenpnpm tokens:buildregeneratesdist/.
Still out of scope (nothing guessed)
--radius-sm/md/lg/xl—calc(var(--radius) ± Npx)expressions living in the@theme inlinemapping layer (globals.css:170-173,@siax/config/tailwind-preset.css), not values. Since--radiusis0, two of them compute to negative radii that CSS clamps to 0.- Spacing scale —
.section-padding,.section-gap,.content-gap(globals.css:293-304) are clamp() literals in class bodies with no custom property behind them. Tokenisable by the same$extensionspattern used for the fluid scale; not done here because nothing asked for it and the naming (section-*vs a genericspace-*scale) is a product decision. - Container widths —
.container-narrow/content/wide/full(680/900/1400/ 1800px,globals.css:634-656), same reasoning. - Animation keyframes — ~30
@keyframesblocks. These are behaviour, not values; the--duration-*/--ease-*scale they should be built from is now tokenised, but most utilities still inlinecubic-bezier(0.32, 0.72, 0, 1)as a literal. Migrating them tovar(--ease-snappy)is a follow-up in siax-io, not here. - The
--color-*/@theme inlinemapping — owned by@siax/config.
Status
Re-derived from the canonical siax-io source 2026-08-09. This package builds
and self-verifies, but @siax/ui's globals.css has not yet been changed
to consume it — a separate, already-assigned follow-up, kept separate so
nothing breaks mid-migration. Right now @siax/tokens is a superset of
@siax/ui/globals.css that additionally agrees with siax.io, checked by
pnpm --filter @siax/tokens test.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| style-dictionary | ^4.4.0 |