[community-group] First-Class Mode Support with `$modes` Array (#348)

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