function
<Component />
return
const
interface
{ }
color.primary
TS
() => {}
spacing.xl
dev
tokens
[ ]
code
<T>
type
async
font.sans
JS
TypeScript
export
[React, ...skills]
components
import
border.radius
props
default
?.
tokens
shadow.lg
await
&&
class
<Props>
accessibility
variables
transition
JSX
</>
theme
motion
( )
hooks
state
utils
wcag
responsive
grid
flex
scale
{ml}
Back to artifacts
Jan 12, 20263 min read

SemVer for Design Variables

A lightweight framing for versioning tokens so design and code stay in lockstep. Part of the Variable Design Standard.

design-systemstokensstandards

The problem

A designer renames color.brand.primary to color.brand.main in Figma. Seems harmless. Three weeks later, 47 components are rendering fallback colors in production because the token sync ran, the build passed, and nobody caught the breaking change.
This happens because design variables change like APIs, but we don't treat them that way. No versioning. No impact signal. No way for engineering to know if a token update is safe to pull or will break half the app.

The framing

SemVer gives designers and engineers the same shared language for change. A contract is just a typed payload with a schema version and a contract version. The version tells teams whether a change is safe, risky, or breaking.
{
  "contractVersion": "1.4.0",
  "schemaVersion": "2025.10",
  "source": "design",
  "publishedAt": "2026-01-15",
  "tokens": {
    "color.brand.primary": {
      "type": "color",
      "value": "#4ea1ff"
    }
  }
}

Release rules

ChangeVersion bumpExample
Rename or remove tokenMAJORcolor.brand.primarycolor.brand.main
Change token typeMAJORspacing.md from string to number
Add new tokenMINORNew color.brand.tertiary
Adjust value onlyPATCH#4ea1ff#3d9aff

Detecting breaking changes

The version bump isn't manual it's computed from the diff. Here's the logic:
type BreakingChange = {
  token: string
  reason: 'removed' | 'renamed' | 'type_changed'
  previous: TokenDefinition
}

function detectBreakingChanges(
  previous: TokenContract,
  next: TokenContract,
): BreakingChange[] {
  const breaking: BreakingChange[] = []

  for (const [key, def] of Object.entries(previous.tokens)) {
    if (!(key in next.tokens)) {
      breaking.push({ token: key, reason: 'removed', previous: def })
    } else if (next.tokens[key].type !== def.type) {
      breaking.push({ token: key, reason: 'type_changed', previous: def })
    }
  }

  return breaking
}

function computeVersionBump(
  previous: TokenContract,
  next: TokenContract,
): 'major' | 'minor' | 'patch' {
  const breaking = detectBreakingChanges(previous, next)
  if (breaking.length > 0) return 'major'

  const newTokens = Object.keys(next.tokens).filter(
    (k) => !(k in previous.tokens),
  )
  if (newTokens.length > 0) return 'minor'

  return 'patch'
}

CI integration

This runs in the design token publish pipeline. If the computed bump is major, the pipeline requires explicit approval before tokens sync to code.
#!/bin/bash
# token-publish.sh

BUMP=$(pnpm vds-cli diff --previous tokens.v1.json --next tokens.v2.json)

if [ "$BUMP" = "major" ]; then
  echo "⚠️  Breaking change detected. Requires approval."
  gh pr comment --body "This token update contains breaking changes. \
    Review required before sync."
  exit 1
fi

pnpm vds-cli publish --bump "$BUMP"

Why this matters

When tokens are versioned like code, you can:
  • Audit changes with a clear diff
  • Validate compatibility in CI before sync
  • Roll back to a known-good version without guesswork
  • Communicate impact in a language every team already understands
Variable drift becomes a predictable release cycle. The spec doesn't just document the change; it enforces the contract.
This is part of the Variable Design Standard, a broader effort to bring infrastructure-level rigor to the design-engineering handoff.
© 2026 Mark Learst.Crafted with precision
privacy
v2026.1.0