Token Modes as a Computable Contract: Checking Light, Dark, and High-Contrast Sets Against the DTCG Resolver
Why mode sets drift and stay hidden
The Design Tokens Resolver Module 2025.10 introduction notes that alternate contexts such as light mode, dark mode, and high contrast color modes, plus sizing and accessibility modes, are “susceptible to combinatorial explosion.” The spec describes a mechanism for deduplicating repeated token values across contexts and enumerating every permutation of them.
This post asks one question: what can a tool, or an AI design agent working from files alone, verify about token modes without rendering anything? The resolver makes structural legality checkable; it does not make visual completeness checkable.
The resolver document in 60 seconds
Root properties
According to the resolver spec (§4.1), the root accepts name (optional string), version (required, YYYY.MM, and it MUST be 2025.10), description (optional), sets (optional map), modifiers (optional map), resolutionOrder (required array), and $schema (optional). The required version and resolutionOrder fields are the two anchors a linter can gate on first.
Sets and the last-wins merge
A set MUST contain a sources array of tokens declared directly, a reference object pointing to a JSON file, or both (§4.1.4). When a set has several sources, they merge in array order, and the last declaration of a token wins. Tools MUST respect that order. A static check can verify the merge by reading source order.
Modifiers and contexts
A modifier MUST declare a contexts map. The spec says a modifier SHOULD have two or more contexts, that tools SHOULD throw on exactly one, and that tools MUST throw on zero (§4.1.5.1). A modifier MUST NOT reference another modifier. If a default is declared, it MUST match one of the contexts keys (§4.1.5.3).
resolutionOrder and reference legality
resolutionOrder accepts any ordering of sets and modifiers. Inline entries need name and type ("set" or "modifier"), and tools MUST throw if either is missing or if a name is duplicated (§4.1.6.1). Only resolutionOrder may reference a modifier. Sets and modifiers MUST NOT reference a modifier, and a reference object MUST NOT point into #/resolutionOrder/... (§4.2.1). Reference objects MUST NOT be circular.
Inputs
Inputs are a JSON-serializable object whose values MUST be strings. A non-string value MUST throw (§5.2). Input keys SHOULD be case-insensitive, but tools MAY decide case sensitivity (§5.1). If a modifier declares no default, the input MUST supply a key for it, or the tool throws (§6.1).
The severity ladder
Three tiers keep the rules honest:
- error: a MUST requirement or MUST-throw condition in the resolver spec.
- warning: a SHOULD-level condition in the resolver spec.
- advisory: tool guidance. The main example is Tokens Studio’s “Themes that switch” guide, which asks that sets in the same theme group contain identical token names and types.
The DTCG resolver spec does NOT itself require identical token names across modes. That identical-names rule is Tokens Studio guidance and belongs in the advisory tier. The resolver’s own merge rule is last-occurrence-wins, and its non-normative orthogonality guidance (modifiers MAY be orthogonal) is all it says on the matter.
Three ways to ship modes
The table compares the three stacks using only the sourced claims.
| Dimension | DTCG Resolver 2025.10 | Tokens Studio themes | Figma variable modes |
|---|---|---|---|
| Structure | Top-level sets and modifiers maps, plus a resolutionOrder array |
Theme groups that combine Token Sets, and themes that can apply together | Modes inside a Variable Collection; the left-most column is the default |
| Modifier rule | Needs contexts; SHOULD have 2 or more; MUST throw at 0; default must match a key |
“Themes that switch”: one theme is active per group at a time | Objects use the default mode until a specific mode is set |
| Duplicate tokens | Merge in array order; last occurrence wins | Guidance: identical names and types across sets in a theme; without themes, the plugin uses “last set wins” | Figma’s modes article: “If two tokens end up with the same normalized name, only the first one encountered will be imported.” |
| Export target | JSON resolver document with string inputs; translation tools produce platform code | Themes configuration file; sd-transforms can generate theme files for all permutations | Variables inside Figma; theme groups export to Figma collections (Pro licence) |
Tokens Studio’s plugin-side theme switching stops working once variables are attached to themes, so designers must use Figma’s native mode switching (Tokens Studio, themes that switch).
Seven rules for mode integrity
Each rule below names the fields it inspects, states its pass and fail conditions, and says what it cannot detect. Every rule reads the resolver document except Rule 6, which also reads the tool’s input.
Rule 1: Version gate (error)
- Reads: root
version. - Pass:
versionis present and equals2025.10. - Fail: missing, malformed, or any other value.
- Cannot detect: whether a given tool implements 2025.10. Style Dictionary says the 2025.10 format “does not have full support yet” and is “a work in progress in v5.”
Rule 2: Set source completeness (error)
- Reads: every entry in
sets, and each set’ssources. - Pass: each set has a
sourcesarray (a spec MUST) whose entries are token declarations or reference objects. - Fail:
sourcesis missing, or an entry is neither a token declaration nor a reference object. - Tool-level check, not a spec MUST: an empty
sourcesarray. - Cannot detect: whether referenced files exist or parse when only the resolver is linted, or whether token values are correct.
Rule 3: Modifier context integrity (error for zero contexts; warning for one)
- Reads: every entry in
modifiers, and each modifier’scontextsmap. - Pass:
contextsis present and non-empty, and has two or more keys. - Fail:
contextsis missing or empty (error, spec MUST-throw), or has exactly one key (warning, spec SHOULD). - Tool-level check, not a spec MUST: a context whose token array is empty. Flag it as a warning in your linter. The spec does not state this case.
- Cannot detect: whether the context names mean what the team intends. “Light,” “dark,” and “high-contrast” are examples, not a controlled vocabulary.
Rule 4: Default must be a declared context (error)
- Reads: each modifier’s
defaultand itscontextskeys. - Pass:
defaultis absent, or equals one of thecontextskeys. - Fail:
defaultnames a key that does not exist. The spec says tools MUST throw. - Cannot detect: whether the chosen default is the right product decision.
Rule 5: Resolution order integrity (error)
- Reads: the
resolutionOrderarray, inline entries’nameandtype, reference pointers, and thesetsandmodifiersmaps. - Pass: every inline entry has
nameand atypeofsetormodifier; names are unique; no pointer lands inside#/resolutionOrder/;#/modifiers/pointers appear only insideresolutionOrder; references form no cycle. - Fail: any violation. A missing or duplicate
name, or a missingtype, is a spec MUST-throw condition (§4.1.6.1). Atypeother thansetormodifierbreaks the spec’s MUST requirement for that field. A reference into#/resolutionOrder/is forbidden by the spec’s MUST NOT rule (§4.2.1), and so is a circular reference. - Cannot detect: whether the ordering produces the cascade the team intends. The spec allows any ordering.
Rule 6: Input contract coverage (error)
- Reads:
modifiers(whether each has adefault), plus the input object supplied to the tool. The input is a separate artifact from the resolver document, so this is the one rule that reads it. - Pass: every input value is a string that matches one of that modifier’s
contextskeys (§6.1); every input key matches a declared modifier; every modifier without adefaulthas an input key. - Fail: a non-string value, a value that matches no context key (for example
{"theme": "purple"}when the contexts are light and dark), an unknown key, or a default-less modifier with no input. - Cannot detect: whether a supplied context suits the end user’s device or preferences. Input keys SHOULD be case-insensitive (§5.1), but tools MAY decide case sensitivity, so a strict linter must state which behavior it uses.
Rule 7: Cross-mode parity (advisory)
- Reads: for one modifier, the token names and types in each context, including names in referenced set files.
- Pass: every context declares the same token names and types. Values may differ.
- Fail: a context omits a name that another context has, or a shared name has a different type.
- Status: advisory only. This is Tokens Studio guidance, not a DTCG requirement (see the severity ladder above). Under the resolver, divergence is not an error.
- Cannot detect: contrast, accessibility, or whether a value is appropriate for its mode. No source reviewed establishes those outcomes.
Assembling the checks
A sensible order is the spec’s own processing order, which runs input validation (§6.1) first, then ordering and resolution, with aliases resolved afterward. A linter can mirror it: check the inputs, then the structure and resolutionOrder, then the advisory parity pass. That order is a tooling choice, not a spec mandate. Map MUST-throw conditions to errors, SHOULD conditions to warnings, and Tokens Studio guidance to advisories.
Tool support is partial
Style Dictionary describes itself as “forward-compatible” with the DTCG specification. Its DTCG page adds that 2025.10 “does not have full support yet” and is in progress for v5, so a team cannot assume the toolchain enforces the resolver today. sd-transforms is described as custom transforms for Style Dictionary that work with tokens exported from Tokens Studio.
What static rules cannot catch
Static JSON checks cannot verify contrast, accessibility, or visual correctness of any mode. Nothing reviewed here establishes those outcomes, and no measurement of them was taken. Static checks also cannot say how often mode sets drift in practice; no adoption or drift figures were fetched.
FAQ
Does the DTCG spec require identical token names across modes? No. The resolver merges with last-occurrence-wins. Identical names and types come from Tokens Studio’s guidance, which is advisory.
Is Style Dictionary ready for 2025.10 resolver documents? Not fully. Its DTCG page says 2025.10 support is still a work in progress for v5.
When should I use a set rather than a modifier?
A set is a collection of tokens that applies directly. A modifier is similar to a set but allows conditional inclusion through its contexts map, and it is referenced only from resolutionOrder (§4.2.1). Use a set for unconditional tokens and a modifier for values that vary by an input-selected context.
Can these rules verify contrast? No. Contrast and accessibility outcomes of any mode are not established by the sources.
How does this map to Figma? Theme groups map to Variable Collections and themes map to Variable Modes, per Tokens Studio’s Figma export docs. Figma’s modes article says variables and modes can represent light, dark, high-contrast, and color-blindness themes.
What happens with duplicate tokens? In the resolver, the last occurrence wins. In Tokens Studio without themes, the plugin’s “last set wins” rule applies. Figma imports only the first of two tokens that normalize to the same name.
The Bottom Line
The resolver document is lintable. Several of its MUST-throw constraints are purely mechanical, so a team can enforce structural integrity of its light, dark, and high-contrast modes with a static linter today. Completeness across modes is a different claim. The spec does not require it. Adopt Tokens Studio’s identical-names guidance as an advisory check if that is your toolchain. Treat the resolver as a contract you enforce yourself, not one the ecosystem enforces for you, because Style Dictionary’s 2025.10 support is still in progress.
How This Guide Was Built
Methodology: we reviewed the official DTCG specs (Resolver, Format, Index), Tokens Studio docs (themes overview, themes that switch, Figma export), Style Dictionary docs (home, DTCG page), and Figma help docs (variables guide, modes). Sources were fetched on 2026-10-31.
This review is based on official documentation, specifications, and vendor docs. We did not run these checks against a live token repository.
