ADR
Unified Token Reference Type — Replace `VariableStyle` and `FigmaStyle` with `TokenReference`
Context
@directededges/anova currently represents references to Figma variables and Figma named styles as two separate types within the Style union:
VariableStyle— a Figma variable reference carryingid,rawValue,name,variableName,collectionName,collectionIdFigmaStyle— a Figma named style reference carryingidandname
Current Style shape:
# types/Styles.tsStyle: string | boolean | number | null | VariableStyle | FigmaStyle | ReferenceValue
VariableStyle: id: string # Figma variable UUID rawValue?: string | number | boolean name?: string variableName?: string # e.g. "DS Color/Text/Primary" collectionName?: string # e.g. "DS Color" collectionId?: string
FigmaStyle: id: string # Figma style UUID name?: string # e.g. "Body/Medium"
ReferenceValueis not a token reference.ReferenceValue—{ "$ref": "#/props/{PropName}" }— is a prop binding: it signals that a style property’s value is driven by a component prop at runtime (e.g.,visibledriven by anisVisibleprop). It is orthogonal to design token references and remains unchanged in theStyleunion.
Serialized output for a style property carrying a reference currently appears as either an object or a plain string, with no discriminator marking it as a reference vs a literal:
backgroundColor: { id: "VAR:123", variableName: "DS Color/Text/Primary" }textStyleId: { id: "STY:456", name: "Body/Medium" }Problems
Implicit reference kind
a property value may be a VariableStyle, a FigmaStyle, or a plain string literal. Consumers cannot distinguish a reference from a raw value without inspecting field shape. FigmaStyle and VariableStyle both look like objects with an id string but carry different semantics.
Figma-biased field names
variableName, collectionName, collectionId, and id (a raw Figma UUID) are Figma API concepts with no equivalents in any platform-neutral token format. Output consumers (anova-kit CLI, platform adapters) cannot treat this representation as platform-independent.
Expanding composite reference surface
typography, effects, and gradient properties now use named-style references (FigmaStyle) alongside their inline structured types (Typography, EffectsGroup, GradientValue). A fourth category — composites referenced by name — currently reuses FigmaStyle, which lacks a $type signal telling consumers what the reference resolves to. Additionally, inline composite values (GradientStop.color, Blur.radius, all variable-bindable Typography sub-fields) use VariableStyle at the leaf level — the same Figma-biased shape described in point 2, requiring the same replacement.
The W3C Design Tokens Community Group format module (Candidate Recommendation, published 28 October 2025, considered stable and intended for implementation) identifies tokens by path, not by source-tool ID, and reserves $-prefixed keys for standard semantics. Figma-specific metadata belongs in a $extensions block per that convention, with keys in reverse domain name notation (§5.2.3). Anova output should be structurally compatible with that direction rather than mirroring Figma internals.
Compact output and human readability
A full TokenReference object ({ $token: "DS Color.Text.Primary", $type: "color" }) is more verbose than the current flat string representation. This is an intentional trade-off: the structured object makes the token type explicit and is machine-parseable without heuristics. A “simplified” profile — where the transformer emits just the $token string instead of the full object — is a valid serialization option for human-readable audit output. That serialization choice belongs in anova-transformer’s output configuration, not in the type contract. TokenReference defines the canonical typed shape; simplified string output is a projection of it.
Decision Drivers
- Type–schema sync: Every type change must have a corresponding schema change. No drift between
types/andschema/is permitted (Constitution I). - No runtime logic: This package declares shapes only. No processing or conditional logic may be added (Constitution II).
- Stable public API — MAJOR for breaking changes: Removing or restructuring
VariableStyleandFigmaStylebreaks consumers that type-check against them. A MAJOR version bump is required (Constitution III). - Platform-independent output: Reference representation must not require consumers to understand Figma variable collections or style IDs to extract a token path.
- Explicit type discrimination: The resolved token type (
color,typography,shadow,effects, etc.) must be statically identifiable without field-shape inspection, using DTCG$typesemantics. - Design tokens format alignment: Reference paths and metadata placement should follow the DTCG convention —
$-prefixed keys for standard fields,$extensionsfor tool-specific data. - Minimal new surface: New types must represent a genuine shared concept.
TokenReferencereplaces two types with one; no net surface expansion.
Options Considered
Option A: Retain VariableStyle and FigmaStyle as-is (Rejected)
Keep both types unchanged. Accept the current dual-type representation for the foreseeable future.
Rejected because:
VariableStyleandFigmaStylehave structurally identical shapes ({ id: string, name?: string }) with no discriminator — consumers must distinguish them by which property they appear on, which is fragilevariableName,collectionName,collectionIdare Figma-internal metadata with no platform-neutral equivalent; they cannot be mapped to token paths without Figma-specific parsing logic- Composite named references (
effects,typography) are currently typed asFigmaStyleeven though they resolve to structured shapes — the absence of a$typesignal creates ambiguity that will only compound as more composite types are added - The platform-independence driver is structurally unsatisfiable without replacing the Figma-centric field set
Option B: TokenReference — unified discriminated reference with DTCG-aligned extensions (Selected)
Introduce a single TokenReference interface with a $type discriminator using DTCG token type values, a $token path field, and Figma-specific metadata isolated in $extensions["com.figma"]. Replace all uses of VariableStyle and FigmaStyle in Style, ColorStyle, and composite property types with TokenReference. Deprecate and eventually remove VariableStyle and FigmaStyle from the public API.
# Option B — new shapeTokenReference: $token: string # DTCG dot-path, e.g. "DS Color.Text.Primary"; usable directly as DTCG alias {DS Color.Text.Primary} $type: color | dimension | string | number | boolean | shadow | gradient | typography | effects # Anova-extended: named reference to an EffectsGroup (multi-shadow + blur); no DTCG equivalent $extensions?: "com.figma": # reverse domain name per DTCG §5.2.3 id: string # Figma variable or style UUID name?: string # Figma name within collection, e.g. "Text/Primary" collectionName?: string # Figma collection name, e.g. "DS Color" (variables only) rawValue?: string | number | boolean # value resolved by Figma at extraction time; no DTCG equivalent
Style: string | boolean | number | null | TokenReference | ReferenceValue
ColorStyle: HexColor | TokenReference | ReferenceValue | GradientValue | null# HexColor = string (see Color strong-typing note in Decision section)Pros:
- Single reference type — no ambiguity between variable and named-style references
$typeuses DTCG token type semantics — values describe what the token resolves to (color,typography,shadow, etc.) rather than how Figma stores it (variable,style,composite); this is platform-neutral by definition$type: "effects"is the only non-DTCG value; it is explicitly documented as an Anova extension forEffectsGroupreferences (multi-shadow + blur) that have no standard DTCG composite equivalent- The Figma variable vs. named-style origin is fully captured by
$extensions["com.figma"]— the presence ofcollectionNamedistinguishes a variable reference from a named-style reference without a top-level Figma-taxonomy field $tokenpath is platform-neutral;variableName/collectionName/collectionIdare retired- Figma UUID is namespaced under
$extensions["com.figma"], consistent with DTCG §5.2.3 reverse domain name notation for tool-specific metadata $extensionskey is open for other tool namespaces without changing the top-level contract
Cons / Trade-offs:
- MAJOR break:
VariableStyleandFigmaStyleare referenced throughoutanova-transformerandanova-kit; both packages must update after this is published $token,$type, and$extensionsuse$prefixes — unconventional in TypeScript interfaces but intentional for DTCG alignment"effects"deviates from DTCG$typevalues; this must be explicitly documented as an extension- Full object form is more verbose than a plain string; compact output is a transformer concern, not a type contract concern (see Context above)
Option C: Additive StyleReference union alias over existing types (Rejected)
Add a kind field to both VariableStyle (kind: "variable") and FigmaStyle (kind: "style") and export a union alias StyleReference = VariableStyle | FigmaStyle. Update Style to include StyleReference alongside the existing members.
Rejected because:
- Retains all Figma-biased field names (
variableName,collectionName,collectionId, Figmaid) - Platform-independence driver is not satisfied — consumers must still parse Figma-internal metadata to extract a usable token path
StyleReferenceis syntactic sugar over an unchanged Figma-specific contract
Option D: String-only token reference { $token: string } without $type (Rejected)
Use a minimal shape { $token: string } for all references (variables, styles, composites), omitting the $type discriminator and $extensions.
Rejected because:
- Without
$type, consumers cannot determine what shape a resolved token value takes without out-of-band knowledge (e.g., cannot distinguish acolortoken from adimensiontoken or aneffectscomposite) - The implicit-reference-kind problem (driver 3 in Context) is only partially resolved
- References to
EffectsGroupandTypographycomposites require explicit$typediscrimination so consumers know to expect a structured shape, not a scalar
Decision
TokenReference replaces both VariableStyle and FigmaStyle as the single type for any style property value that references a design token — whether that token is a Figma variable, a named style, or a composite reference (effects, typography). The new type carries three fields:
$token(a DTCG dot-path usable directly as a DTCG alias),$type(a DTCG token type discriminator), and- an optional
$extensions["com.figma"]block for Figma-specific metadata such as the source UUID, collection name, and resolved scalar.
This eliminates the implicit ambiguity between VariableStyle and FigmaStyle objects, isolates all Figma internals behind a namespaced extension key, and provides an explicit $type signal for every reference kind — including composite references (effects, typography) that previously had no discriminator at all.
VariableStyle and FigmaStyle are deprecated in v0.11.0 and scheduled for removal in the next MAJOR release. This change ships alongside ADR 007 (007-token-reference-config.md), which replaces the three-field variables/simplifyVariables/simplifyStyles config surface with a single format.tokens enum governing how TokenReference objects are serialized in transformer output.
Deferred decisions to subsequent ADRs
- Color strong-typing alignment with DTCG
- Shadow shape alignment with DTCG
- Custom configuration of Anova output for “compact” vs “detailed” data structures to account for human readability and use cases
TokenReference Shape
# BeforeVariableStyle: id: string # Figma variable UUID rawValue?: string | number | boolean name?: string variableName?: string # e.g. "DS Color/Text/Primary" collectionName?: string # e.g. "DS Color" collectionId?: string
FigmaStyle: id: string # Figma style UUID name?: string # e.g. "Body/Medium"
Style: string | boolean | number | null | VariableStyle | FigmaStyle | ReferenceValueColorStyle: string | VariableStyle | FigmaStyle | ReferenceValue | GradientValue | null
# AfterTokenReference: $token: string # DTCG dot-path, e.g. "DS Color.Text.Primary" $type: "color" | "dimension" | "string" | "number" | "boolean" | "shadow" | "gradient" | "typography" | "effects" # Anova extension: EffectsGroup reference; no DTCG equivalent $extensions?: "com.figma": # reverse domain name per DTCG §5.2.3 id: string # Figma variable or style UUID name?: string # Figma name within collection, e.g. "Text/Primary" collectionName?: string # Figma collection, e.g. "DS Color" (variables only) rawValue?: string | number | boolean # scalar resolved by Figma at extraction time
Style: string | boolean | number | null | TokenReference | ReferenceValueColorStyle: string | TokenReference | ReferenceValue | GradientValue | null# string arm: "#RRGGBB" or "#RRGGBBAA" — pattern-constrained in schema onlyWhat Changes and Where
| Concept | types/ | schema/ | Bump |
|---|---|---|---|
Add TokenReference interface | Styles.ts | styles.schema.json (new #/definitions/TokenReference) | MINOR |
Style union: replace VariableStyle | FigmaStyle with TokenReference | Styles.ts | styles.schema.json (StyleValue oneOf) | MAJOR |
ColorStyle union: same replacement | Styles.ts | styles.schema.json (ColorStyleValue oneOf) | MAJOR |
Deprecate VariableStyle and FigmaStyle | Styles.ts (@deprecated) | styles.schema.json (deprecated: true) | MAJOR |
effects property: FigmaStyle → TokenReference | Styles.ts | styles.schema.json (EffectsStyleValue) | MAJOR |
typography property: FigmaStyle → TokenReference | Styles.ts | styles.schema.json (TypographyStyleValue) | MAJOR |
Shadow: rename x/y → offsetX/offsetY; add inset?; replace VariableStyle in all fields | Effects.ts | styles.schema.json (Shadow definition) | MAJOR |
EffectsGroup: merge dropShadows + innerShadows → shadows (discriminated by inset) | Effects.ts | styles.schema.json (EffectsGroup definition) | MAJOR |
Blur.radius: VariableStyle → TokenReference ($type: "dimension") | Effects.ts | styles.schema.json (Blur definition) | MAJOR |
GradientStop.color: VariableStyle → TokenReference ($type: "color") | Gradient.ts | styles.schema.json (GradientStop definition) | MAJOR |
Typography sub-fields (11 fields): VariableStyle → TokenReference | Styles.ts | styles.schema.json (Typography definition) | MAJOR |
ColorStyleValue string arm: add hex pattern constraint | (none — type stays string) | styles.schema.json | MINOR |
Gradient stop, blur, and typography inline sub-fields
Beyond the named-style replacements above, VariableStyle also appears inside inline composite values wherever individual sub-fields can be variable-bound in Figma:
# Before — VariableStyle in inline sub-fieldsGradientStop: position: number color: string | VariableStyle # variable-bound stop color
Blur: visible: boolean radius: number | VariableStyle # variable-bound blur radius
Typography: fontSize: number | VariableStyle letterSpacing: number | VariableStyle lineHeight: string | number | VariableStyle textCase: string | VariableStyle textDecoration: string | VariableStyle paragraphIndent: number | VariableStyle # ... (all variable-bindable sub-fields follow the same pattern)
# After — TokenReference replaces VariableStyle in all inline sub-fieldsGradientStop: position: number color: string | TokenReference # $type: "color"
Blur: visible: boolean radius: number | TokenReference # $type: "dimension"
Typography: fontSize: number | TokenReference letterSpacing: number | TokenReference lineHeight: string | number | TokenReference textCase: string | TokenReference textDecoration: string | TokenReference paragraphIndent: number | TokenReference # ... (all variable-bindable sub-fields follow the same pattern)This is distinct from the named-style FigmaStyle → TokenReference replacements for effects and typography properties: those operate at the property level (the whole value is a reference). These sub-field replacements operate at the leaf level — the inline composite is present, but one or more of its constituent values is variable-bound.
Notes:
GradientStop.colorwith$type: "color"carries the token path for the color variable;$extensions["com.figma"].rawValueholds the resolved hex string captured at Figma extraction time.Blur.radiuswith$type: "dimension"follows the same pattern asShadowoffset/blur/spread fields.Typographysub-fieldVariableStylearms are fully replaced. Sub-fields with noVariableStylearm (e.g.,fontFamily) are unchanged.
References
-
$tokenand$typeare the only fields a platform adapter needs.$tokenis the DTCG alias path;$typedetermines how the resolved value is formatted.$extensions["com.figma"]is extraction provenance for tooling and round-tripping — not required for platform code generation. -
$tokenuses.as the segment separator per DTCG alias syntax (Format §7.1.1). The original Figma slash-path is preserved in$extensions["com.figma"].name.$tokenDTCG alias Figma origin "DS Color.Text.Primary"{DS Color.Text.Primary}collection "DS Color", name"Text/Primary""Body.Medium"{Body.Medium}style name "Body/Medium""Elevation.Shadow.Card"{Elevation.Shadow.Card}style name "Elevation/Shadow/Card" -
"effects"is the sole non-DTCG$typevalue — no DTCG composite covers a multi-shadow + blur group. It must be documented as an Anova extension and treated as non-portable. -
Variable vs. named-style origin is encoded in
$extensions["com.figma"]: presence ofcollectionNameidentifies a variable reference; its absence identifies a named-style reference.
Downstream Impact
| Consumer | Impact | Action required |
|---|---|---|
anova-kit | Recompile; update any code reading VariableStyle- or FigmaStyle-shaped objects, shadow offset fields, dropShadows/innerShadows keys, gradient stop colors, blur radius, or typography sub-fields | Replace reads of variableName, collectionName, id (style name) with $token and $type checks; Figma UUID now at $extensions["com.figma"].id; variable vs. named-style origin: check presence of $extensions["com.figma"].collectionName; shadow offsets: .x/.y → .offsetX/.offsetY; shadow role: dropShadows/innerShadows → shadows.filter(s => !s.inset) / shadows.filter(s => s.inset); gradient stop .color and blur .radius now string | TokenReference; typography sub-fields (fontSize, letterSpacing, etc.) now number | TokenReference where previously number | VariableStyle |
Semver Decision
Version bump: Incorporated into v0.11.0 (MAJOR)
Justification: Removing VariableStyle and FigmaStyle from the Style and ColorStyle union types, from effects and typography property types, and renaming Shadow.x/Shadow.y to Shadow.offsetX/Shadow.offsetY all break consumers that type-check against those interfaces. Per Constitution III: “Removing or renaming an exported type or a named field within a type is a breaking change and MUST follow semantic versioning.” This change is batched into v0.11.0, which carries MAJOR character, consistent with the typography composite change (ADR 005) already accepted in this release.
Consequences
- All style property values that reference a design token (variable or named style) carry an explicit
$typediscriminator using DTCG token type semantics, eliminating implicit type ambiguity. The Figma variable vs. named-style origin is encoded solely in$extensions["com.figma"]. - The
$tokenpath field stores a DTCG dot-path (e.g."DS Color.Text.Primary") usable directly as a DTCG alias. The original Figma slash-path is recoverable from$extensions["com.figma"].collectionNameandname. - Figma UUIDs are namespaced under
$extensions["com.figma"].id, isolating Figma-specific metadata per DTCG §5.2.3 reverse domain name notation, opening the$extensionsblock to other tool namespaces without future breaking changes. ReferenceValue({ "$ref": "#/props/..." }) remains unchanged — it is a prop binding, not a token reference, and continues to serve its distinct role in theStyleunion.- Hex color values in
ColorStylegain schema-level pattern validation; TypeScript type is unchanged. Full DTCG color object support in$extensions["com.figma"].rawValueis deferred. Shadow.x/Shadow.yrenamed toShadow.offsetX/Shadow.offsetYper DTCG §9.6;Shadow.inset?: booleanadded.EffectsGroup.dropShadowsandEffectsGroup.innerShadowsmerged into a singleEffectsGroup.shadows?: Shadow[]—inseton each item discriminates drop vs inner. Consumers readingdropShadows/innerShadowsmust update toshadows.filter(s => !s.inset)/shadows.filter(s => s.inset).Blur.radius,GradientStop.color, and all variable-bindableTypographysub-fields (fontSize,letterSpacing,lineHeight,textCase,textDecoration,paragraphIndent, etc.) replaceVariableStylewithTokenReference(carrying the appropriate$typefor the resolved value —"dimension"for numeric offsets/radii,"color"for stop colors, etc.). Inline composite values (theEffectsGroup,GradientValue, andTypographyobjects themselves) are unchanged; only the leaf arms that previously acceptedVariableStyleare updated.- Compact string output for token references (
"DS Color.Text.Primary"instead of the full object) is a valid projection for human-readable views, but is a transformer serialization concern — it does not change this type contract. VariableStyleandFigmaStyleremain as@deprecatedexports inv0.11.0and are removed in the next MAJOR release.- Any tool validating output against
schema/styles.schema.jsonmust update to thev0.11.0schema to pass validation afteranova-transformeradopts the new reference shape.