11 · Tokens beyond CSS¶
The moment a decision has two consumers, it has two copies. Two copies drift.
You have seen the drift: the brand colour changes in the Figma file and nobody tells the iOS team, so the app is a version behind for eight months and nobody notices because nobody can see both at once. The problem is not communication. It is that the decision was stored in two places.
Which is why a design system is a data problem before it is a CSS problem. CSS is one output.
The format¶
DTCG — the Design Tokens Community Group format — is the W3C-incubated standard for writing design decisions as JSON. It reached its first stable version, 2025.10, on 28 October 2025.
That date matters when you read anything about design tokens. Before it, every tool had its own dialect, which is why older tutorials contradict each other and why so much tooling only half-works together.
Two things in 2025.10 are load-bearing here.
Colour is an object, not a hex string.
"$value": {
"colorSpace": "oklch",
"components": [0.585, 0.2186, 285],
"alpha": 1,
"hex": "#755cf5"
}
oklch is one of fourteen legal colour spaces, so a colour solved in OKLCH survives the trip
instead of being flattened to 8-bit sRGB on the way out. Chapter 2's whole argument would be
undone by a format that only carried hex.
Theming lives in a separate document. The format module has no concept of modes at all. For
four years every tool invented its own answer and a $modes proposal sat open; in October 2025
the group settled on resolvers — token sets, plus a document saying which set applies in
which context.
What a token carries besides its value¶
This is the part that turns a colour list into a design system.
"9": {
"$value": {
"colorSpace": "oklch",
"components": [0.585, 0.2186, 285],
"hex": "#755cf5"
},
"$description": "the solid — the filled button, the brand moment …",
"$extensions": {
"io.github.jvoltci.nilam": {
"role": "step 9",
"cssVar": "--brand-9",
"oklch": {
"L": 0.5850000000000067,
"C": 0.21861984218200592,
"h": 285
},
"contrast": {
"against": "color.brand.1", "ratio": 4.31,
"floor": 3, "holds": true, "wcag": "1.4.11"
},
"ink": {
"token": "color.brand.ink", "polarity": "light",
"ratio": 4.57, "floor": 4.5, "wcag": "1.4.3"
}
}
}
}
Three things are doing work:
$descriptionis the role, not a colour name. "border of a control — WCAG 1.4.11 governs this at 3:1" tells a consumer what they may use it for.#755cf5does not.$extensionscarries the proof. The ratio that was measured, the floor it had to clear, the WCAG clause, and the full-precision OKLCH the solver produced. So a designer opening the file can see why the value is what it is, without reading the source.cssVarties every token back to the stylesheet, which is what lets the test suite assert that the two artefacts contain exactly the same tokens, in both directions.
The one field to actually read¶
$extensions[…].ink.polarity on step 9. It says whether near-white or near-dark text is
legible on the solid — and it flips between modes and between hues, as chapter 5 showed. If you
export tokens and drop that field, every consumer downstream will hard-code white.
Why each platform needs a different shape¶
The naive assumption is that exporting tokens is translation: same structure, different syntax. It is not, because each platform has a different idea of what a colour is.
SwiftUI has no dynamic-colour primitive. There is no light-dark(). So each token has to be
a Color backed by UIColor(dynamicProvider:) on iOS or NSColor(name:dynamicProvider:) on
macOS. Emit a flat light-only namespace and the mode inversion is silently lost — which matters
more in nilam than in most systems, because step 9 is a different lightness in each mode rather
than the same colour with a different opacity.
Compose wants two factories, not one object, for the same reason:
That is the shape Material 3 uses, and it is the shape because a single palette cannot express a value that inverts.
Figma variables are 8-bit sRGB. So the OKLCH is quantised on the way in. The worst error
across nilam's palette is 0.002 in OKLab — invisible, and not lossless. Figma also has no
variable type for a cubic-bezier and no effect variable, so the easings and shadows do not go at
all. And it has no viewport, so a fluid clamp() type size has no single true value to send.
The honest part: say what you dropped¶
A token export that does not say what it dropped is a lie, because the consumer will assume completeness and build on a value that was never there.
nilam's omissions, and the reason for each:
| Not exported | Why |
|---|---|
--tracking-* |
unit is em; DTCG dimension allows only px and rem, and 2025.10 has no string type to fall back on |
--measure |
unit is ch, same reason |
| easings, shadows | Figma has no variable type for either |
One value is changed rather than omitted: --text-* is a fluid clamp() with no DTCG type,
so the exported dimension is the value at a 1280px viewport — the wide end. Both ends and
the fluid term are kept in $extensions, and the $description says so on every one.
The omission list ships inside the document, at
$extensions["io.github.jvoltci.nilam"].omittedFromScaleCss, so you can see what is missing
without reading a web page. And the test suite asserts that every custom property in the
stylesheet is either exported or on that list — a token cannot go missing quietly.
How nilam handles it¶
One function per target, and nothing hand-maintained:
Two shapes of DTCG, for two different jobs:
| Call | You get | Use it for |
|---|---|---|
toDtcg(palette) |
a resolver document — one shared set, one mode modifier, two contexts |
archiving the whole system in one file |
toDtcg(palette, { mode: 'light' }) |
a flat token document, single mode | Style Dictionary, Figma importers, anything reading only the format module |
Prefer the flat form unless you know your tool reads resolvers. Resolver support is thin.
The non-colour scales — type, space, radius, lines, focus, motion, stacking, elevation — are
read out of nilam.scale.css rather than retyped. That is the structural point of the whole
chapter: there is one place each number lives, and the export is a view of it rather than a
second copy.
And it is asserted in both directions. dtcg.test.mjs is 6,063 assertions comparing the
export against the stylesheet and the stylesheet against the export, because a token present in
one and absent from the other is exactly the failure a one-way check cannot see.
What is not verified¶
Stated as plainly as the rest:
- The Swift output type-checks with
swiftc -typecheck. The Kotlin output has not been compiled. - Nobody has run these tokens through Figma against a live file. The request body matches the documented shape; that is all that is claimed.
- There is no published JSON Schema for DTCG token documents, only for resolvers. So the flat
documents are checked against the spec's structural rules by hand, and they carry no
$schema—$schemais not one of the eight$-properties 2025.10 reserves, and putting it on a group would be inventing syntax. - Style Dictionary v5 resolver support could not be confirmed from its documentation. That is unverified, not confirmed absent.
Next: how you know you are right — and what a test can never catch.
Reference: Platform export has every adapter with a working call. What the token export does not carry is the same omission list on the limitations page, where it belongs.