Version your design system like an API

A removed token doesn't throw an error. Your cards just lose their background. How to version tokens so that never ships as a patch.

Your tokens are an API

A design system has consumers the same way a library does. App code reads your CSS variables. A Figma library binds to your variables. AI agents read your token names and write code against them. When a token disappears, all of them break.

Worse, CSS won't tell you. If --color-bg-card stops existing, background: var(--color-bg-card) doesn't throw an error. The declaration becomes invalid at computed-value time and the property falls back to its inherited or initial value. For background, that's transparent. Your cards quietly lose their fill and nobody finds out until a customer sends a screenshot.

So treat the token set like a public API and version it with semver: MAJOR.MINOR.PATCH. A major means "something you rely on changed, read the notes". A minor means "new things, nothing you use broke". A patch means "same meaning, better values".

What makes a release major

The numbering is easy. Deciding which bucket a change belongs in takes judgement, so this is the table we use:

ChangeBumpWhy
Remove a tokenMajorEvery usage silently breaks
Rename a token, old name goneMajorSame as a removal, from the consumer's side
Rename a token, old name kept as an aliasMinorNothing breaks yet; it's a deprecation
Change what a semantic alias meansMajorThe value is valid, but every usage is now wrong
Remove or rename a component prop or slotMajorCode that compiled yesterday doesn't
Add tokens, components or a themeMinorNobody was using them yet
Deprecate a token that still worksMinorA warning, not a break
Tweak a value, every contrast pair still passesPatchSame meaning, better value
Repoint an alias to another step, same meaningPatche.g. muted text moves one step darker to pass AA
Tweak a value and a contrast pair now failsNot a patchFix it, or ship it as a break with a note

The row that catches people is changing what an alias means. Say --color-surface used to be the card background and you decide it's the page background now, with cards moving to --color-surface-raised. No token was removed. Every build passes. And every card in every product is now the same colour as the page. That's a major, even though a diff of the token file looks tiny.

The opposite case: a brand refresh that changes every primary value is technically a patch or a minor. No names changed, no meanings changed. Ship it as a major anyway. Version numbers are also a way of telling people to look, and a new brand colour is something they should look at.

Deprecate with an alias first

Never go straight from "exists" to "gone". Rename in two steps, across two releases:

  1. In a minor (say 2.4.0), add the new name and keep the old one as an alias pointing at it. Mark it deprecated and say when it goes.
  2. In the next major (3.0.0), remove the alias.
:root {
  --color-surface-raised: var(--neutral-0);

  /* @deprecated since 2.4.0: use --color-surface-raised. Removed in 3.0.0. */
  --color-bg-card: var(--color-surface-raised);
}

If your source of truth is DTCG-format JSON, it has a $deprecated property for exactly this, and tools that read the format can surface it:

{
  "color": {
    "bg-card": {
      "$type": "color",
      "$value": "{color.surface-raised}",
      "$deprecated": "Use color.surface-raised. Removed in 3.0.0."
    }
  }
}

How long should the window be? Long enough that a team with a busy roadmap can schedule the change. We'd give it at least a quarter. The alias costs you one line; a forced emergency migration costs someone a week.

Give people a way to do the rename in one go:

# find every use of the old name
grep -rn -- "--color-bg-card" src/

# replace it (GNU sed; on macOS use sed -i '')
grep -rl -- "--color-bg-card" src/ | xargs sed -i 's/--color-bg-card/--color-surface-raised/g'

[!WARNING] Check for longer names that share the prefix before you run a blind replace. --color-bg-card-hover would become --color-surface-raised-hover, which may not exist.

Changelogs people read

Most design system changelogs are commit logs with a heading on top. Nobody reads them, because they're organised around what the maintainers did instead of what the reader has to do.

  • Breaking changes go first, each with the exact migration step.
  • Every entry names the token. "Updated colours" tells the reader nothing.
  • Value changes show old and new. If contrast was the reason, give the ratio.
  • Group under plain headings: Breaking, Added, Changed, Deprecated, Removed, Fixed. The Keep a Changelog convention is close enough that people already know it.
  • Leave out internal refactors that don't change any output.
## 3.0.0 - 2026-09-11

### Breaking
- Removed `--color-bg-card` (deprecated in 2.4.0). Use `--color-surface-raised`.
- `--color-surface` is now the page background only. Cards use `--color-surface-raised`.

### Added
- `--space-inline-xs` (4px) for icon-to-label gaps.

### Fixed
- Dark theme `--color-text-muted`: `--neutral-500` → `--neutral-400`. It measured 3.75:1 on `--color-surface`; it now measures 6.96:1.

Announcing a breaking change

A major release should never be the first time someone hears about a change. By the time 3.0.0 ships, everything in it should have been deprecated in a 2.x release.

  1. Ship the deprecations in a minor, with the removal version written next to each one.
  2. Publish a migration guide: a table of old name to new name, plus the replace script.
  3. Give consumers a way to find their own usages. A grep in the guide is fine; a lint rule is better.
  4. Say when the major lands, then land it on that date.
  5. Remind people that pinning to ^2.4.0 keeps them on 2.x until they choose to move. Caret ranges don't cross a major.
  6. Post it where your consumers actually look, which is rarely the design system site. The team channel, the release notes in their dependency update PR.

Done this way, nobody is surprised by 3.0.0.

Where Flash's History fits

In Flash, Analytics in the editor's left nav holds History: Timeline, Releases and Compare sit under it in the side menu. Flash checkpoints your system automatically while you work, so there's always an autosave to fall back on. When you want a named point, Save a version asks what changed and for a release type: Patch, Minor or Major. It shows the number the save will take before you confirm.

  • Compare takes a From and a To version and shows what moved, grouped by section. That's the raw material for the changelog above.
  • Releases compares each named version with the previous release rather than the last autosave, so it reads like a changelog instead of a list of tiny edits.
  • Restore asks "Restore v{semver}?" first and saves a copy of where you are now, so you can come straight back.
  • Renaming a version keeps its number, because people may already be citing it.

[!NOTE] Flash numbers the version from the release type you pick. It doesn't decide whether a change is breaking. The table at the top of this post is how you make that call.

Version history and diffs are part of Team. The roles side, who can edit and who can only read, is covered in team roles and history.