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

This would help document multi-modal tokens. If we can do mode specific
referencing, that would make it a step above most current tooling.

On Sun, 9 Nov 2025 at 18:27, Kevin Muldoon ***@***.***> wrote:

> *caoimghgin* created an issue (design-tokens/community-group#348)
> <https://github.com/design-tokens/community-group/issues/348>
> Before starting
>
>    - 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
> <https://github.com/design-tokens/community-group/pull/298>)
> ------------------------------
> TL;DR
>
> This proposal introduces a $modes array to address the variant
> discoverability challenge in PR #298
> <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/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 <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/298>'s goals—it *builds
> upon the foundation* by adding the missing discoverability mechanism.
> ------------------------------
> The Challenge: Completing PR #298
> <https://github.com/design-tokens/community-group/pull/298>'s Vision What
> PR #298 <https://github.com/design-tokens/community-group/pull/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:
>
> {
>   "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:
>
> // The available option: blacklist approach with fragility concernsconst 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
>
> {
>   "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
> <https://github.com/design-tokens/community-group/pull/298>'s Goals
>
> PR #298 <https://github.com/design-tokens/community-group/pull/298>
> wanted *"base values with variants"*. $modes provides this without
> ambiguity:
>
> *PR #298 <https://github.com/design-tokens/community-group/pull/298>
> approach (Section 6.2 example):*
>
> {
>   "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:*
>
> {
>   "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
>
> // 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
>
> // Current (Section 6.2 pattern) - requires heuristicsconst modes = Object.keys(token).filter(k => !k.startsWith('$') && k !== 'metadata')// ↑ May become fragile when spec evolves or user adds metadata
> // Proposed - always worksconst modes = token.$modes?.map(m => m.name) ?? []// ↑ Guaranteed correct, future-proof
>
> 2. Order Preservation & Simple Defaults
>
> // Arrays guarantee order (JSON objects don't)const modes = token.$modes.map(m => m.name)
> // No separate $root keyword - first element is always defaultconst defaultValue = token.$modes?.[0].$value ?? token.$value
>
> 3. Self-Documenting & Iterable
>
> // Is this multi-mode?if (token.$modes) {
>   console.log(`Available modes: ${token.$modes.map(m => m.name).join(', ')}`)}
> // Find specific modeconst darkMode = token.$modes?.find(m => m.name === 'dark')
>
> 4. Forward-Compatible
>
> Adding new metadata fields (future spec versions) *never conflicts* with
> mode detection:
>
> {
>   "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):*
>
> // 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:*
>
> // Clear, robust APIconst modes = token.$modes.map(m => m.name)// Result: ['default', 'light', 'dark'] - guaranteed correct
>
> Getting a Mode Value
>
> *Current DTCG:*
>
> const darkValue = token.dark?.$value  // Is 'dark' a mode or metadata?
>
> *Proposed:*
>
> 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
>
> /** * 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:
>
> 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
>
> {
>   "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
>
> {
>   "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:*
>
> {
>   "$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
>    <https://github.com/design-tokens/community-group/pull/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 <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/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
> <https://github.com/design-tokens/community-group/pull/298> by providing
> the missing piece:* a standardized mechanism for parsers to discover
> variants without heuristics.
> ------------------------------
> Full Example
>
> {
>   "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:*
>
> const surfaceColor = tokens.colors.surfaceconst 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 workconst brandColor = tokens.colors.brand.$value  // '#0066cc'
>
> ------------------------------
>
> *Contact:*
> Kevin Muldoon ***@***.*** <https://github.com/caoimghgin>)
> GitHub: https://github.com/caoimghgin/token-distillery
>
> *References:*
>
>    - DTCG Spec 2025.10: https://www.designtokens.org/TR/drafts/format/
>    - PR #298 <https://github.com/design-tokens/community-group/pull/298>
>    (Groups and Aliases): 25e14e5
>    <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
>
>    - I have read and agree to abide by the CODE_OF_CONDUCT
>    <https://github.com/design-tokens/community-group/blob/CODE_OF_CONDUCT.md>
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/design-tokens/community-group/issues/348>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AEKS36EH4KA2ILBVEEHOU7L337LXLAVCNFSM6AAAAACLTMFUA6VHI2DSMVQWIX3LMV43ASLTON2WKOZTGYYDKNRXHA4TCOA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>


-- 
GitHub Notification of comment by nesquarx
Please view or discuss this issue at https://github.com/design-tokens/community-group/issues/348#issuecomment-3509381476 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 04:49:57 UTC