- From: Kevin Muldoon via GitHub <noreply@w3.org>
- Date: Mon, 10 Nov 2025 00:26:52 +0000
- To: public-design-tokens-log@w3.org
caoimghgin has just created a new issue for https://github.com/design-tokens/community-group:
== First-Class Mode Support with `$modes` Array ==
### Before starting
- [x] I have searched [existing issues](https://github.com/design-tokens/community-group/issues) and there is not already an open issue on the subject.
### Summary
# First-Class Mode Support with `$modes` Array
**Date:** 2025-11-09
**Proposed By:** Kevin Muldoon
**Target:** Design Tokens Community Group (DTCG) Specification
**Relates To:** DTCG 2025.10, Section 6.2 "Root Tokens in Groups" (PR #298)
---
## TL;DR
This proposal introduces a `$modes` array to address the variant discoverability challenge in PR #298's `$root` pattern. The `$modes` array provides explicit enumeration of mode variants, positional defaults (first element), and forward-compatible parsing—completing the work started in PR #298 by adding the missing standardized discovery mechanism.
---
## Executive Summary
This proposal adds first-class mode support to the DTCG specification through a new `$modes` array. This addresses an **implementation challenge** in Section 6.2 "Root Tokens in Groups" that creates implementation fragmentation risk.
**The challenge:** PR #298 (October 2025) introduced `$root` for "base values within groups" with "variants or extensions" as sibling keys. However, **the spec currently lacks a standardized mechanism for parsers to programmatically discover which sibling keys represent variants**. This requires implementers to use heuristics that may become fragile when the spec evolves.
Users must either maintain a blacklist of reserved keys (like `$type`, `$description`) to filter out, manually enumerate which keys are modes, or rely on type-checking heuristics—all of which present challenges for forward compatibility.
**The solution:** An explicit `$modes` array that:
- ✅ Eliminates parsing ambiguity (variants are explicitly enumerated)
- ✅ Provides simple default semantics (first element is default, replacing `$root`)
- ✅ Maintains backward compatibility (optional, non-breaking)
- ✅ Enables forward-compatible extensibility (new spec fields never conflict with modes)
- ✅ Complements (not replaces) the existing Resolver specification
**Relationship to `$root`:** This proposal **replaces** the `$root` pattern introduced in PR #298. `$modes` addresses both challenges (`$root` addressed only one):
1. ✅ Base/default value (via array position, not keyword)
2. ✅ Variant discovery (via explicit enumeration)
---
## Why This Matters Now
PR #298 was merged just weeks ago (October 15, 2025). Given this recent timing, **we have an optimal window to address this implementation challenge before parsers ship with divergent approaches**.
This proposal doesn't reject PR #298's goals—it **builds upon the foundation** by adding the missing discoverability mechanism.
---
## The Challenge: Completing PR #298's Vision
### What PR #298 Introduced
From the [PR #298 commit message](https://github.com/design-tokens/community-group/commit/25e14e5) (October 15, 2025):
> **Key Features Added:**
> - Root tokens using reserved `$root` name for **base values within groups**
> - [...] allowing for **variants or extensions**
**Section 6.2 "Root Tokens in Groups"** example:
```json
{
"color": {
"accent": {
"$root": {
"$type": "color",
"$value": "#dd0000"
},
"light": {
"$type": "color",
"$value": "#ff2222"
},
"dark": {
"$type": "color",
"$value": "#aa0000"
}
}
}
}
```
### The Remaining Implementation Challenge
**Critical question:** How does a parser programmatically discover that `light` and `dark` are the "variants or extensions"?
**The spec currently lacks a standardized approach for this.**
There is no standardized way to distinguish variant keys from metadata keys. A parser encountering the `accent` group above must use heuristics to guess which object keys represent variants:
```typescript
// The available option: blacklist approach with fragility concerns
const variantKeys = Object.keys(accent).filter(key =>
key !== '$root' &&
key !== '$type' &&
key !== '$description' &&
key !== '$deprecated' &&
key !== '$extensions' &&
key !== '$extends'
// What about future spec additions?
// What about custom metadata?
)
```
**Fundamental challenges:**
1. ❌ **Not queryable:** Cannot reliably enumerate all variants
2. ❌ **Blacklist may become stale:** When spec adds new `$properties`, existing parsers may break
3. ❌ **Cannot distinguish user metadata:** No way to tell `light` (a variant) from `id` (metadata)
4. ❌ **Forward compatibility challenge:** Future spec additions may create breaking changes for parsers
5. ❌ **Risks fragmentation:** Tools may invent different heuristics
6. ❌ **May affect interoperability:** Same file, different interpretations
### Multi-Mode Tokens Require Discoverability
If you support multi-mode tokens (light/dark, desktop/mobile, default/high-contrast), parsers **must** be able to:
1. **Query:** "What modes exist for this token?"
2. **Enumerate:** "Give me all mode names"
3. **Iterate:** "Process each mode"
4. **Distinguish:** "Is `light` a mode or metadata?"
Section 6.2's pattern currently lacks a standardized approach for this, requiring implementers to rely on heuristics that may become fragile as the spec evolves.
---
## The Proposal: `$modes` Array
### Core Structure
```json
{
"surface": {
"$type": "color",
"description": "Primary surface color",
"id": "S:abc123", // User metadata (A proposed 'id' key is added in future) - NOT confused with a mode
"$modes": [
{
"name": "light",
"$value": "#ffffff",
"id": "M:123:1" // Optional mode-specific metadata
},
{
"name": "dark",
"$value": "#000000",
"id": "M:123:2"
},
{
"name": "high-contrast",
"$value": "#f0f0f0",
"id": "M:123:3"
}
]
// First element ($modes[0]) is always the default
}
}
```
**Key properties:**
- **Explicit namespace:** Modes live in `$modes` array, never collide with metadata
- **Positional default:** Index 0 is always default (no `$root` keyword needed)
- **Self-documenting:** Presence of `$modes` immediately signals multi-mode token
- **Queryable:** `token.$modes.map(m => m.name)` always works
### How This Addresses PR #298's Goals
PR #298 wanted **"base values with variants"**. `$modes` provides this without ambiguity:
**PR #298 approach (Section 6.2 example):**
```json
{
"accent": {
"$root": { "$value": "#dd0000" }, // ← Base value (via keyword)
"light": { "$value": "#ff2222" }, // ← Variant (but how to discover?)
"dark": { "$value": "#aa0000" } // ← Variant (but how to discover?)
}
}
```
**Challenges:**
- ✅ Base value is explicit (`$root`)
- ❌ Variants are not easily discoverable
- ❌ No standardized way to query all variants
- ❌ Forward compatibility concerns
**`$modes` approach:**
```json
{
"accent": {
"$type": "color",
"$modes": [
{ "name": "default", "$value": "#dd0000" }, // ← Base value (index 0)
{ "name": "light", "$value": "#ff2222" }, // ← Variant (explicit)
{ "name": "dark", "$value": "#aa0000" } // ← Variant (explicit)
]
}
}
```
**Improvements:**
- ✅ Base value is explicit (first element)
- ✅ Variants are discoverable (`$modes.map(m => m.name)`)
- ✅ Queryable, iterable, forward-compatible
- ✅ Simpler (one pattern instead of `$root` + siblings)
### Migration from `$root` Pattern
```json
// Before (Section 6.2 pattern)
{
"accent": {
"$root": { "$value": "#dd0000" },
"light": { "$value": "#ff2222" },
"dark": { "$value": "#aa0000" }
}
}
// After ($modes pattern)
{
"accent": {
"$type": "color",
"$modes": [
{ "name": "light", "$value": "#ff2222" }, // First = default
{ "name": "dark", "$value": "#aa0000" }
]
}
}
```
**Migration note:** In this example, if the `$root` value was unique (not duplicated in any mode), it would become `{ "name": "default", "$value": "#dd0000" }` at index 0. By making position define default, we eliminate the need for `$root` while making variants discoverable.
---
## Benefits of `$modes` Array
### 1. Eliminates Parsing Ambiguity
```typescript
// Current (Section 6.2 pattern) - requires heuristics
const modes = Object.keys(token).filter(k => !k.startsWith('$') && k !== 'metadata')
// ↑ May become fragile when spec evolves or user adds metadata
// Proposed - always works
const modes = token.$modes?.map(m => m.name) ?? []
// ↑ Guaranteed correct, future-proof
```
### 2. Order Preservation & Simple Defaults
```typescript
// Arrays guarantee order (JSON objects don't)
const modes = token.$modes.map(m => m.name)
// No separate $root keyword - first element is always default
const defaultValue = token.$modes?.[0].$value ?? token.$value
```
### 3. Self-Documenting & Iterable
```typescript
// Is this multi-mode?
if (token.$modes) {
console.log(`Available modes: ${token.$modes.map(m => m.name).join(', ')}`)
}
// Find specific mode
const darkMode = token.$modes?.find(m => m.name === 'dark')
```
### 4. Forward-Compatible
Adding new metadata fields (future spec versions) **never conflicts** with mode detection:
```json
{
"surface": {
"$type": "color",
"$modes": [ /* ... */ ],
"customMetadata": "value", // ← Won't be mistaken for a mode
"$futureSpecField": "data" // ← Same
}
}
```
---
## Implementation Impact
**For spec maintainers:**
- ✅ Resolves implementation ambiguity before tools ship incompatible approaches
- ✅ Simplifies spec (one pattern instead of `$root` + ad-hoc siblings)
- ✅ Reduces future support burden (fewer "how do I parse modes?" questions)
**For tool builders:**
- ✅ Clear API (`token.$modes.map(m => m.name)`)
- ✅ No guesswork about which keys are modes vs. metadata
- ✅ Future-proof parsing (new spec fields don't break mode detection)
**For design system teams:**
- ✅ Predictable behavior across tools (Figma → Tokens Studio → build pipeline)
- ✅ Order preservation for mode priority/fallbacks
- ✅ Explicit defaults without magic conventions
---
## Comparison: Current vs. Proposed
### Discovering Modes
**Current DTCG (Section 6.2 pattern):**
```typescript
// Blacklist approach (may become fragile)
const modes = Object.keys(accent).filter(key =>
key !== '$root' &&
key !== '$type' &&
key !== '$description'
// ↑ What about future spec additions?
// ↑ What about user metadata?
)
```
**Proposed:**
```typescript
// Clear, robust API
const modes = token.$modes.map(m => m.name)
// Result: ['default', 'light', 'dark'] - guaranteed correct
```
### Getting a Mode Value
**Current DTCG:**
```typescript
const darkValue = token.dark?.$value // Is 'dark' a mode or metadata?
```
**Proposed:**
```typescript
const darkMode = token.$modes?.find(m => m.name === 'dark')
const darkValue = darkMode?.$value
```
---
## Non-Goals
To clarify scope, this proposal does **not**:
- ❌ Replace the Resolver specification (complementary, different use cases)
- ❌ Mandate multi-mode support for all tokens (single-mode via `$value` remains valid)
- ❌ Prescribe which mode dimensions to use ("light/dark" vs. "compact/spacious" is user choice)
- ❌ Require breaking changes to existing DTCG implementations
---
## TypeScript Definitions
```typescript
/**
* Mode-specific value with explicit name
*/
export type DTCGModeValue<T = any> = {
name: string // Mode name (e.g., "light", "dark")
$value: T
id?: string // Optional vendor-specific identifier
[metadata: string]: any // Allows custom metadata
}
/**
* Token with multi-mode support (array-based)
*/
export type DTCGToken = {
$type: string
description?: string
// Single-mode tokens (backward compatible)
$value?: any
// Multi-mode tokens (array, first element is default)
$modes?: DTCGModeValue[]
[metadata: string]: any // Token-level metadata
}
```
---
## Migration Path
### Phase 1: Additive (Non-Breaking)
Add `$modes` as an **optional** array field. Spec states:
- If `$modes` is present, it contains mode variants as an array
- If `$modes` is absent, `$value` is used (current behavior)
- First element of `$modes` array is always the default mode
### Phase 2: Tooling Adoption
Tools can support both patterns:
```typescript
function getTokenValue(token: DTCGToken, modeName?: string): any {
// New pattern (array-based)
if (token.$modes) {
if (modeName) {
const mode = token.$modes.find(m => m.name === modeName)
return mode?.$value
}
return token.$modes[0].$value // Default to first mode
}
// Legacy pattern
return token.$value
}
```
---
## Real-World Use Cases
### Use Case 1: Figma Variables Export
```json
{
"surface": {
"$type": "color",
"id": "VariableID:123:456",
"$modes": [
{
"name": "light",
"$value": "#ffffff",
"id": "123:456:1"
},
{
"name": "dark",
"$value": "#000000",
"id": "123:456:2"
}
]
}
}
```
### Use Case 2: Responsive Typography
```json
{
"heading-large": {
"$type": "typography",
"$modes": [
{
"name": "desktop",
"$value": { "fontSize": { "value": 48, "unit": "px" } }
},
{
"name": "mobile",
"$value": { "fontSize": { "value": 24, "unit": "px" } }
}
]
}
}
```
---
## Relationship to DTCG Resolver Specification
The DTCG Resolver specification addresses a **different challenge** than `$modes`:
| Aspect | `$modes` (This Proposal) | Resolver Specification |
|--------|--------------------------|------------------------|
| **Challenge** | Parsing ambiguity when authoring/exporting multi-mode tokens | Runtime context selection across combinatorial dimensions |
| **Use Case** | Design tool → code (Figma Variables export) | Code → runtime (theme switching in applications) |
| **Scope** | Single dimension (e.g., theme modes within one token) | Multi-dimensional (theme × size × a11y) |
| **Format** | Single file, explicit mode enumeration | Multiple files + resolver config |
### When to Use Each
**Use `$modes` when:**
- ✅ Exporting from design tools (Figma, Sketch, Tokens Studio)
- ✅ Token has multiple values for a **single dimension** (theme OR size OR a11y)
- ✅ All modes are known at authoring time
**Use Resolver when:**
- ✅ **Multiple independent dimensions** need to combine (theme × size × a11y)
- ✅ De-duplication across contexts is critical
- ✅ Runtime context selection based on user preferences
### They Complement Each Other
`$modes` addresses the **authoring-time parsing challenge** identified in Section 6.2.
Resolver addresses the **runtime combinatorial challenge**.
A complete workflow might use both:
1. **Author** tokens with `$modes` (design tools)
2. **Transform** to Resolver format (build step)
3. **Consume** via Resolver at runtime (application)
---
## Design Decisions and Tradeoffs
### Why Array Instead of Object?
**Considered:**
```json
{
"$modes": {
"light": { "$value": "#fff" },
"dark": { "$value": "#000" }
}
}
```
**Array chosen because:**
- ✅ Order preservation guaranteed
- ✅ First element = default (no extra field needed)
- ✅ Mode name as explicit `name` property
- ✅ Natural iteration patterns
- ✅ Each mode can have its own metadata
### Why Not Extend `$root`?
**Considered:** Keep `$root` but add `$modeKeys: ["light", "dark"]`
**Rejected because:**
- ❌ Redundant (mode names listed twice)
- ❌ Error-prone (easy for `$modeKeys` to get out of sync)
- ❌ Still mixes modes with metadata in same namespace
- ❌ More complex than explicit `$modes` array
---
## Proposed Spec Changes
If accepted, the following updates to the DTCG specification are recommended:
### 1. Revise Section 6.2 "Root Tokens in Groups"
**Option A:** Replace with `$modes` example (shows explicit variant enumeration)
**Option B:** Add explanatory note to current example:
> **Note:** This example shows sibling mode keys for illustration. However, parsers currently lack a standardized mechanism to distinguish `light` and `dark` from metadata keys. For standardized mode parsing, see Section 6.3 `$modes` specification.
### 2. Add New Section 6.3: "Multi-Mode Tokens"
Add complete specification of `$modes` array:
- Structure requirements (array of objects with `name` and `$value`)
- Default semantics (first element is default)
- Validation rules (unique mode names, required properties)
- Interaction with `$value` (mutually exclusive)
- TypeScript type definitions
- Examples covering common use cases
### 3. Appendix: Relationship to Resolver
Cross-reference explaining when to use `$modes` vs. Resolver specification, including conversion strategies.
---
## Questions for DTCG
The committee's feedback on these specific questions would be valuable:
1. **Challenge validation:** Does the parsing ambiguity in Section 6.2 align with implementation challenges you've heard from tool builders?
2. **Solution approach:** Is an explicit `$modes` array the right solution, or would you prefer a different approach?
3. **`$root` relationship:** Should `$modes` **replace** `$root` (as proposed here), or should both patterns coexist? We're open to the committee's perspective on the best path forward given PR #298's goals.
4. **Backward compatibility:** Should `$value` remain for single-mode tokens indefinitely, or eventually require `$modes` for consistency?
5. **Naming:** Is `$modes` acceptable, or would `$variants`, `$contexts`, or another term better align with DTCG terminology?
6. **Timeline:** What's the process for considering this addition?
---
## Conclusion
**PR #298 introduced `$root` for "base values with variants"—this proposal builds upon that foundation** by addressing the remaining implementation challenge: parsers currently lack a standardized mechanism to discover which sibling keys are variants.
**This creates a challenging situation** where tools must use heuristics that may become fragile as the spec evolves.
**Given PR #298's recent timing (October 2025), we have an optimal window to address this implementation challenge before tools ship with divergent approaches.**
**This proposal offers:**
- Explicit `$modes` array to address parsing ambiguity
- Simplified spec (eliminates `$root` keyword, uses positional defaults)
- Backward compatibility (optional, non-breaking)
- Forward-compatible extensibility (new spec fields never conflict)
- Complementary relationship with Resolver spec (different challenges, complementary solutions)
- Alignment with existing tool terminology (Figma "modes", Tokens Studio)
- Robust, type-safe parsing with queryable, discoverable modes
**This builds upon the foundation laid by PR #298 by providing the missing piece:** a standardized mechanism for parsers to discover variants without heuristics.
---
## Full Example
```json
{
"design-tokens": {
"colors": {
"surface": {
"$type": "color",
"description": "Primary surface background",
"id": "figma:variable:abc123",
"$modes": [
{
"name": "light",
"$value": "#ffffff",
"id": "figma:mode:abc123:1"
},
{
"name": "dark",
"$value": "#1a1a1a",
"id": "figma:mode:abc123:2"
}
]
},
"brand": {
"$type": "color",
"description": "Brand primary color (same across modes)",
"$value": "#0066cc"
}
}
}
}
```
**Consumer code:**
```typescript
const surfaceColor = tokens.colors.surface
const modes = surfaceColor.$modes.map(m => m.name) // ['light', 'dark']
const defaultValue = surfaceColor.$modes[0].$value // '#ffffff'
const darkValue = surfaceColor.$modes.find(m => m.name === 'dark')?.$value // '#1a1a1a'
// Single-mode tokens still work
const brandColor = tokens.colors.brand.$value // '#0066cc'
```
---
**Contact:**
Kevin Muldoon (@caoimghgin)
GitHub: https://github.com/caoimghgin/token-distillery
**References:**
- DTCG Spec 2025.10: https://www.designtokens.org/TR/drafts/format/
- PR #298 (Groups and Aliases): https://github.com/design-tokens/community-group/commit/25e14e5
- Figma Variables: https://help.figma.com/hc/en-us/articles/15339657135383
- Tokens Studio: https://tokens.studio/
---
**Status:** Draft Proposal
**Last Updated:** 2025-11-09
**Version:** 2.0 (Short Form)
### Version
Third Editor’s Draft
### Problem & motivation
_No response_
### Prior art
_No response_
### Pros & cons
_No response_
### Alternatives
_No response_
### Required
- [x] I have read and agree to abide by the [CODE_OF_CONDUCT](https://github.com/design-tokens/community-group/blob/CODE_OF_CONDUCT.md)
Please view or discuss this issue at https://github.com/design-tokens/community-group/issues/348 using your GitHub account
--
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config
Received on Monday, 10 November 2025 00:26:53 UTC