Skip to content

Design Tokens Are a Count, Not a Convention

Design tokens need measurable adoption, not just a shared file. Learn how to audit rendered components across light and dark themes and identify styling inconsistencies.

Published
Updated
AI-generated, checked by independent automated gates, no human editorial review.
How this guide was made

AI-generated. Written by an automated system, checked by independent automated gates, and published without human editorial review. How we label and verify.

Forty generated pages, each of which looked right on its own. Put two side by side and the card edges disagreed; put four together and the muted text was three different greys. No page failed review, and no single file explained the drift — it was spread across forty files that had each been allowed to write a colour directly. Then someone opened a page nobody had checked in dark mode, and the body text came back near-black on near-black. A design token file had existed for weeks.

What we assumed a design token layer bought us

We assumed declaring the vocabulary was the work. The file was there, the names were sensible, and we treated adoption as a cultural matter — a convention generators would follow once reminded. Reminding does not produce a number.

Nobody was disobeying. A generator writes a shadow because the component needs one and inventing a value is cheaper than consulting a vocabulary it was never forced to open. They collect where a stylesheet linter does not look, because they are not stylesheet: an inline style attribute, an arbitrary bracket value inside a utility class, a CSS string assembled at runtime. Each produces a visual result without naming anything, which is why they accumulate quietly.

Adherence has a countable form

The useful part is not the advice that you should have design tokens; anyone building a multi-page product already agrees. It is that compliance is measurable on the shape of the output: you can count the places in generated markup that produce appearance without going through the named layer, and the result is an integer rather than a judgement. It should be zero.

Zero is a decision, not enthusiasm. Any threshold above it is a budget, and budgets get spent: the first allowed literal is argued for on merit and the second cites the first. A scan that matched no files also reports all zeros and is indistinguishable from a clean run, so an empty scan has to fail.

Counting also changes the remedy. Before declaring any names we census what the output already contains — distinct colour literals, spacing values, font sizes, shadows and radii, normalised so one colour written two ways collapses to one entry — or you cannot later show the number went down. That census is where the vocabulary comes from, rather than a template someone else’s product needed: a name earns its place when two components would otherwise disagree about the value. Ours settled at twenty-nine names across seven families, each filled colour shipping the ink that sits legibly on it so the pairing is never improvised at the call site.

Dark mode is where the drift becomes visible

The near-black-on-near-black page was not an isolated slip. A dark theme is generally correct on exactly the pages someone happened to open, and that is structural. A theme-scoped name with no dark declaration does not throw: it falls back to its light value and renders — legibly enough where it was reviewed, wrong everywhere else.

What is missing is a parity check: every theme-scoped name declared in both states, compared as a set difference rather than an impression, with both sets required to be non-empty, since two empty sets also differ by nothing. So you declare a name in the dark block even when its value matches the light one: repeating the value is what makes an absence a bug instead of a shrug.

Do not let the palette be its own witness

A contrast check that reads its colour values out of your own definition file will tell you the palette is internally consistent, which was never the question. What matters is whether the ink-on-ground pairings that actually shipped clear their threshold, and only the render knows which pairings those are. Both colours have to come off the rendered page, and the thresholds apply to computed size and weight, not to what the stylesheet asked for.

The only honest measurement is computed style read back from a rendered page, in both themes. It catches what no static pass can see — correct CSS, green build, and a name that resolves to nothing at runtime because it was declared on a scope the node is not inside. An unresolved reference falls silently back to the user-agent default and the compiler is satisfied. Build output is a claim about the rendered state, not the state itself.

What follows for anyone generating a lot of UI

A visual rule that cannot be counted is an opinion, not a gate. The test for any appearance convention is three questions: what integer does it produce, off which artifact, and what does the publish step do with it. If the answer to the third is that it lands in a summary, you have a metric, and metrics stop nothing from shipping.

The inversion worth running before trusting any of this: ask what a determined generator would do to produce a visual result without naming a token, then confirm your counter sees that. That question separates a violation counter from a lint config that happens to be green.

The runnable procedure — the census, the violation classes, the parity and read-back checks, and the guards that catch each of these failures — lives in the member library.

How we know

What the standards and design system docs say

Token work now has a shared format. In October 2025 the W3C community group behind the format announced that the Design Tokens specification reaches first stable version. The final Format Module defines a token as information associated with a human readable name, at minimum a name and value pair, and states that the specification is considered stable. It is a Community Group report, not a W3C Standard, but it gives tools a common file to read.

Themes got their own module. The Resolver Module describes a way to work with design tokens in multiple contexts, with light and dark color themes as the example. Its resolution order is significant, with tokens later in the list overriding any tokens that came before them. That rule cuts both ways. A dark context that omits a name does not raise anything; the earlier value simply stays in force. The format makes theme parity expressible. It does not enforce it.

The browser explains why the failures stay quiet. MDN’s reference for var() describes when a fallback is used, including when a custom property has not been declared on a rule that applies to the element and is not registered with an initial-value descriptor. Without a usable fallback, the declaration becomes invalid at computed-value time. MDN’s guide to custom properties explains that the property then takes its inherited value if it inherits, or its initial value if it does not. The declaration is not dropped and nothing is reported.

Contrast thresholds are also exact. The WCAG 2.2 explanation of Success Criterion 1.4.3 sets 4.5:1 for normal text and 3:1 for large text, where 14 point bold text is judged to be large enough for the lower ratio, along with 18 point regular text. The ratios are hard limits, and the document is plain about rounding: 4.499:1 would not meet the 4.5:1 threshold. It also converts points to CSS pixels, with 14pt and 18pt coming to approximately 18.5px and 24px. A check that reads font sizes from the stylesheet rather than the computed style can put text on the wrong side of that line.

How this connects to our token gate

The near-black-on-near-black page in this article is what the CSS rules above predict. A name that does not resolve where the node sits falls back silently, and a theme that leaves out a name keeps whatever came first. Neither shows up in a build log.

The standards give you a shared vocabulary and exact thresholds. They leave the counting to you. That is why this article measures computed styles from the rendered page in both themes, treats any untokenized literal as a violation, and requires every theme-scoped name to be declared in both states.

A checklist for a design token gate

  1. Census the colors, spacing, font sizes, shadows and radii already in your output before you name anything.
  2. Store tokens in the DTCG format so tools agree on structure.
  3. Keep components pointing at named tokens, never at raw values.
  4. Declare every theme-scoped name in each theme, even when the value repeats, and compare the sets.
  5. Scan inline styles, arbitrary utility values and runtime-built CSS, not just stylesheets.
  6. Fail the scan when it matched no files.
  7. Measure contrast from computed colors and computed font size on the rendered page, in both themes, and do not round.

More questions

Is the DTCG format an official W3C standard?

No. It is a final report from a W3C Community Group. The report itself says it is not a W3C Standard nor is it on the W3C Standards Track. It is stable, though, and tool support is the practical reason to use it.

Why didn’t the browser warn us about a missing token?

Because by the rules of CSS nothing failed. An unresolved var() without a fallback leaves the declaration invalid at computed-value time, and the property quietly takes an inherited value or its initial value. Only a read of computed style will show it.

Grounded in: our own token-gate runs behind design-token-system v1.1, 2026-09-04 — a generated page set whose dark theme rendered near-black text on a near-black ground on pages nobody had opened. Verified: computed colour, background, padding, font size, border colour and radius read back from the live DOM in both themes and matched against the declared token values, with contrast ratios taken from the rendered colours rather than the definition file. The runnable procedure lives in the member library.

Related guides

Skill Spotlights · Published

Agent Skills: The Four Bytes Four Gates Missed

Four inspection gates checked 22 agent skills but missed four incorrect bytes. This case shows why teams need different verification methods, not simply more checks of the same kind.

Skill Spotlights · Published

What the JetEngine REST API actually reaches

JetEngine's REST API exposes two routes with different authentication behavior. Learn what the live endpoint tests proved, and where reads and writes stop.

Skill Spotlights · Published

The Slide Deck Check That Read Four-Pixel Text

A slide deck passed its checks despite four-pixel text. Learn to measure rendered legibility at delivery resolution and verify citations against their exact source spans.