DESIGN.md Token Reference: How Cross-Token Resolution Works in google-labs-code/design.md
Yes, DESIGN.md tokens can reference other tokens using curly-brace syntax like {colors.primary}, and the resolution works through a three-phase symbol table process that handles nested chains, detects circular dependencies, and enforces configurable depth limits.
The google-labs-code/design.md specification allows DESIGN.md token references to create dependencies between values, enabling single-source-of-truth architectures where base tokens drive derived values. Understanding how the CLI resolves these references is essential for debugging complex token hierarchies and avoiding circular dependency errors.
Curly-Brace Syntax for Token References
In a DESIGN.md file, tokens reference other tokens using curly-brace reference syntax defined in docs/spec.md:
colors:
primary: "#1A1C1E"
primary-60: "{colors.primary}"
text-inverse: "{colors.primary-60}"
components:
button-primary:
backgroundColor: "{colors.primary-60}"
rounded: "{rounded.md}"
The reference path inside the braces (e.g., colors.primary-60) points to any other token defined in the YAML front-matter. References can appear anywhere a literal value is allowed, including colors, dimensions, typography definitions, and component properties.
The Three-Phase Resolution Process
According to the source code in packages/cli/src/linter/model/handler.ts, the CLI resolves token references through a symbol table that maps fully-qualified token names to their values. The resolution occurs in three distinct phases.
Phase 1: Building the Symbol Table
The handler first parses all tokens and populates the symbol table. Primitive values (hex colors, px/rem dimensions, typography objects) are stored directly, while reference strings are stored raw for later resolution.
Relevant code from lines 57-64 and 86-94:
if (typeof raw === 'string' && isTokenReference(raw)) {
// Store raw reference for later resolution
symbolTable.set(`colors.${name}`, raw);
} else if (isValidColor(raw)) {
const resolved = parseColor(raw);
colors.set(name, resolved);
symbolTable.set(`colors.${name}`, resolved);
}
This phase ensures every token path exists in the table, whether as a concrete value or an unresolved reference string.
Phase 2: Chained Reference Resolution
After initial parsing, the system walks the token trees again to resolve references. The resolveReference function (lines 77-96) handles this recursively:
function resolveReference(symbolTable, path, visited, depth = 0) {
if (depth > MAX_REFERENCE_DEPTH) return null;
if (visited.has(path)) return null; // circular reference
visited.add(path);
const value = symbolTable.get(path);
if (value === undefined) return null;
if (typeof value === 'string' && isTokenReference(value)) {
const innerPath = value.slice(1, -1);
return resolveReference(symbolTable, innerPath, visited, depth + 1);
}
return value; // concrete token (color, dimension…)
}
This algorithm supports nested references (a token that itself references another token) by recursively following chains until it reaches a concrete value or hits a safety limit.
Phase 3: Component Construction
During component evaluation, property values undergo the same resolution process. Lines 85-100 illustrate this:
if (isTokenReference(rawValue)) {
const refPath = rawValue.slice(1, -1);
const resolved = resolveReference(symbolTable, refPath, new Set());
if (resolved !== null) {
properties.set(propName, resolved);
} else {
unresolvedRefs.push(rawValue);
properties.set(propName, rawValue);
}
}
If resolution fails, the raw reference string is preserved and recorded as an unresolved reference for the linter to report.
Safety Guarantees and Validation
The resolution process provides several safeguards against malformed references and infinite loops:
- Cycle Detection: The
visitedSet inresolveReferenceprevents infinite loops; circular references returnnulland trigger linter errors. - Depth Limits:
MAX_REFERENCE_DEPTHandMAX_TOKEN_NESTING_DEPTH(defined inpackages/cli/src/linter/spec-config.ts) protect against excessively deep chains. - Type Validation: After resolution, the system verifies that resolved values match expected types (e.g.,
resolved.type === 'color'for color tokens). - Error Reporting: Unresolved or invalid references are added to the
findingsarray and surfaced by the CLI.
Practical Examples
Simple Token References
colors:
primary: "#1A1C1E"
primary-60: "{colors.primary}"
After Phase 2 resolution, colors.primary-60 resolves to the same hex value #1A1C1E as colors.primary.
Chained Reference Resolution
colors:
base: "#112233"
shade: "{colors.base}"
tint: "{colors.shade}"
The resolver follows the chain tint → shade → base and returns the concrete hex #112233.
Component Usage
components:
button-primary:
backgroundColor: "{colors.primary-60}"
rounded: "{rounded.md}"
padding: "12px"
During Phase 3, the handler looks up colors.primary-60 (already resolved to a color object) and rounded.md (a dimension object). The final component definition contains the actual resolved values, ready for downstream exporters like Tailwind or DTCG.
Circular Reference Detection
colors:
a: "{colors.b}"
b: "{colors.a}"
resolveReference detects the cycle when visited.has(path) returns true, returns null, and the linter records an error in the findings array.
Summary
- DESIGN.md token references use curly-brace syntax (
{path.to.token}) to point to other tokens. - Resolution occurs in three phases: symbol table construction, chained reference resolution, and component construction.
- The
resolveReferencefunction inpackages/cli/src/linter/model/handler.tshandles recursive lookups with cycle detection via avisitedSet. - Depth limits (
MAX_REFERENCE_DEPTH) prevent runaway resolution chains. - Failed resolutions are preserved as raw strings and reported as linter errors.
Frequently Asked Questions
What is the exact syntax for referencing tokens in DESIGN.md?
Use curly braces containing the full token path: {colors.primary} or {rounded.md}. This syntax is enforced by the isTokenReference predicate in the linter's model specification.
How does the system handle circular references between tokens?
The resolveReference function tracks visited paths using a Set. If it encounters a path already in the set, it returns null to break the cycle, and the linter reports the error.
What is the maximum depth for token reference chains?
The depth is limited by MAX_REFERENCE_DEPTH and MAX_TOKEN_NESTING_DEPTH, configured in packages/cli/src/linter/spec-config.ts. Exceeding these limits causes the resolver to return null.
Where is the reference resolution logic implemented?
The core implementation lives in packages/cli/src/linter/model/handler.ts, specifically in the resolveReference function (lines 77-96) and the three-phase model building process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →