- From: nesquarx via GitHub <noreply@w3.org>
- Date: Mon, 10 Nov 2025 04:49:56 +0000
- To: public-design-tokens-log@w3.org
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