# 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. - Source: https://flashdsm.com/blog/versioning-a-design-system - Published: 2026-09-11 - Author: Flash DSM Team - Track: Notes - Reading time: 5 min read --- ## 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: | Change | Bump | Why | |---|---|---| | Remove a token | Major | Every usage silently breaks | | Rename a token, old name gone | Major | Same as a removal, from the consumer's side | | Rename a token, old name kept as an alias | Minor | Nothing breaks yet; it's a deprecation | | Change what a semantic alias *means* | Major | The value is valid, but every usage is now wrong | | Remove or rename a component prop or slot | Major | Code that compiled yesterday doesn't | | Add tokens, components or a theme | Minor | Nobody was using them yet | | Deprecate a token that still works | Minor | A warning, not a break | | Tweak a value, every contrast pair still passes | Patch | Same meaning, better value | | Repoint an alias to another step, same meaning | Patch | e.g. muted text moves one step darker to pass AA | | Tweak a value and a contrast pair now fails | Not a patch | Fix 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. ```css :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: ```json { "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: ```bash # 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. ```md ## 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, **Version History** in the editor's left nav opens the History view, with Timeline, Compare and Releases tabs. 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](/blog/team-roles-and-history).