DesignDesign system versioning: which changes break a consumer
SemVer only protects consumers once the public API is written down. Narrowing a prop type is major; dropping a CSS variable they set often is too.

Design system versioning is the practice of numbering every release of a shared component library so that a team using it can tell, from the number alone, whether upgrading will change their product without asking. The rule behind the number is Semantic Versioning: a major release for anything that breaks a consumer, a minor release for additions, a patch for fixes. The hard part is the first clause. What breaks a consumer of a design system goes well past a renamed prop.
The question matters the day a second product depends on the system. Below: what a design system's public API contains, which changes break it and which do not, where visual changes fit, and how to ship a breaking change without stranding the teams still on the old version.
The 30-second version
A change is breaking when a consumer who upgrades without touching their own code gets a different result: a build error, a type error, a layout that moved, a failed accessibility audit, a test selector that no longer matches. Removing a component, removing a prop or narrowing a prop's type is always major. Moving spread props to another element, changing the display mode of a children container, dropping a CSS custom property consumers set, or adding a main landmark is often major. Adding a component, adding an optional prop, broadening a type and deprecating something are minor. A breaking change ships after at least one minor release that deprecates the old path, with a migration guide and, where the change is mechanical, a codemod.
Why does the version number carry so much weight?
Because package managers act on it with no human in the loop. When you run npm install, npm saves the dependency with a caret prefix by default, so version 1.2.3 is written as ^1.2.3 (npm config, save-prefix). A caret range accepts every minor and patch release below the next major: ^1.2.3 resolves to anything from 1.2.3 up to, but not including, 2.0.0 (node-semver). A breaking change published as 1.8.0 reaches every consumer on their next clean install, and nobody chose to take it.
The Semantic Versioning 2.0.0 specification makes one demand before any of this works: software using it must declare a public API, in code or in documentation, and the declaration should be precise and complete. Most design systems skip that step. They version the package and leave the contract implicit, so every behaviour a consumer can observe becomes part of the contract anyway. Hyrum Wright, a Google engineer, wrote it down as a law: with enough users, "all observable behaviors of your system will be depended on by somebody" (Hyrum's Law).
What is the public API of a design system?
More than the props table. A consumer can depend on any of these seven surfaces, and each one needs a written answer to the question "can we change this in a minor?".
- Component and prop names. The import path, the export name, every prop and the values it accepts.
- Prop types. In TypeScript the type is the contract. A union of three strings promises that a fourth will never be needed.
- Rendered structure. Which element is the root, which element receives spread props, how children are laid out.
- Styling hooks. Class names, CSS custom properties and data attributes a consumer can target.
- Tokens. Names first, then values. A renamed token breaks every stylesheet that references it.
- Accessibility semantics. Roles, landmarks, labels and focus behaviour, which page-level audits read.
- Visual values. Colour, type and space, around which other teams build their layouts.
Writing this list down is the step SemVer asks for. Our component API rules cover the first three surfaces. The other four usually live nowhere until something breaks.
Which changes break a consumer?
GitHub's Primer team publishes the most detailed public table we know of, mapping each kind of change to its version bump (Primer React versioning guide). The rows below follow it, with what the consumer actually sees after upgrading.
| Change | Bump | What the consumer sees |
|---|---|---|
| A component or prop is removed | Major | Build or type error on upgrade |
| A prop's type is narrowed (any string becomes three allowed strings) | Major | Type errors on values that used to work |
| A dependency moves to a new major version | Major | Peer conflicts or two copies of one package |
| Spread props move to a different element | Potentially major | Handlers and layout props land on the wrong node |
| The children container changes display, block to flex | Potentially major | Whitespace collapses, children reflow |
| A component stops reading a CSS custom property consumers set | Potentially major | Overrides stop working, with no error |
| A component adds or removes a main or contentinfo landmark | Major | Duplicate or missing landmark in axe audits |
| A data-component attribute is renamed or removed | Major | Tests and analytics selectors stop matching |
Two rows deserve a closer look. The display change is Primer's own example: a button whose label is "Save" followed by a span reading "changes" switches from block to flex, the whitespace between the two collapses, and the button reads "Savechanges". No type checker catches it. The custom property row turns on intent. If a consumer sets --example-component-gap and the component starts reading --gap, the override does nothing after the upgrade. Primer treats component-level custom properties as private unless they are documented as public, which is exactly the declaration SemVer asks for.
Which changes do not?
A minor release adds without taking away. Adding a component, adding an optional prop, and broadening a type (a number prop that now also accepts a string) are all minor in Primer's table, because every value that worked before still works. Deprecating a component or a prop is minor too: SemVer requires a minor release when part of the public API is marked deprecated, and the removal waits for the next major.
A patch fixes behaviour that contradicts the documentation. The trap is the bug consumers have built around. If a dropdown has always closed on scroll and three products rely on it, the fix is a behaviour change for those three. We ship those as minor, with a changelog line that names the old behaviour, so nobody learns about it from a support ticket.
Are visual changes breaking changes?
Sometimes, and this is where most versioning policies go quiet. In 2018 Nathan Curtis of EightShapes argued that colour, typography and space each need their own criteria for breaking change, since adopters build layouts on top of them. "Few system teams document such criteria," he wrote (Visual Breaking Change in Design Systems). His example of a safe change: darkening a primary button's background to raise its contrast.
The test we apply is dimensional. A change that keeps every box the same size is minor: a new shade inside the same contrast band, a new shadow, a radius tweak. A change that alters dimensions ships as major or behind an opt-in flag: a line-height that adds 2 px to every table row, a spacing token that pushes a card grid from four columns to three, a font swap with wider glyphs that wraps a button label onto two lines. Consumers see these in screenshots, never in builds, so visual regression tests on the consumer's side are the only reliable alarm.
How do you ship a breaking change without stranding consumers?
In five steps, each one taken from a system that publishes its process.
- Deprecate in a minor first. The SemVer FAQ asks for at least one minor release containing the deprecation before the major that removes it. Mark it in the types, warn in development builds, and put the replacement in the docs.
- Announce the window. Atlassian's design system commits to removing no generally available feature without a clearly announced deprecation period, and to making breaking changes only in major releases, which "should be infrequent" (Atlassian release phases).
- Ship a codemod for the mechanical part. MUI's v6 upgrade rewrites the old Grid props into the new
sizeandoffsetshape with one command,npx @mui/codemod@latest v6.0.0/grid-v2-props(MUI v6 migration guide). Shopify ships a migrator with each Polaris major for the same reason (Polaris v11 to v12 guide). - Write the migration guide for the rest. A codemod handles renames. It cannot handle a layout that now reflows, so the guide lists every visual change with a before and an after.
- Make the bump a decision on every change. Primer requires a changeset file on every pull request that changes public API or runtime behaviour, declaring patch, minor or major (Changesets). The bump gets argued in review, next to the diff, instead of guessed on release day.
One version for the library, or one per component?
Curtis describes both models (Versioning Design Systems). With one package, a breaking change in any component bumps the whole library to the next major, even for consumers who never import that component. With one package per component, as Atlassian does, a button can sit on one major while a modal sits on another, and consumers upgrade only what changed. The cost moves into dependencies: a component that wraps another must re-release whenever its dependency does, and a consumer can end up shipping two versions of the same primitive in one bundle.
For a system with a handful of consumers, one package is easier to reason about, and a whole-library major is a scheduling problem more than a technical one. Per-component versions pay off when consumers upgrade on very different calendars. The bump rules above hold in both models.
Adjacent concepts
Versioning is one part of running a shared system. Governing a design system across ten consumer projects covers who decides what ships. Component drift covers what happens when consumers stop upgrading and copy components instead. Documentation that gets read is where the public API declaration belongs. And design tokens compared with CSS variables explains why a renamed token reaches further than a renamed prop.
Sources
- Semantic Versioning 2.0.0
- Primer React: versioning
- npm docs: config, save-prefix
- node-semver: caret ranges
- Hyrum's Law
- Nathan Curtis: Visual Breaking Change in Design Systems
- Nathan Curtis: Versioning Design Systems
- Atlassian Design System: release phases
- MUI: upgrade to v6
- Shopify Polaris: migrating from v11 to v12
- Changesets
Frequently asked questions
It depends on what the value moves. Renaming a token is always breaking, because every stylesheet that references the old name stops resolving. A new value that keeps dimensions identical, such as a darker brand shade inside the same contrast range, is usually a minor. A new value that changes size or flow, such as a spacing or line-height token, can reflow every layout built on it and deserves a major or an opt-in flag. Primer treats a switch to a token that only exists in the next major of its token package as breaking, unless the old value ships as a fallback.
Not once a second product depends on it. SemVer says that in major version zero anything may change at any time and the public API should not be considered stable, so the number tells a consumer nothing about risk. npm's caret also behaves differently below 1.0: ^0.2.3 accepts only patches below 0.3.0, so every minor turns into a manual upgrade for every consumer. Releasing 1.0.0 is the moment the public API gets declared, which is the part of versioning that protects people.
SemVer sets a floor, not a duration: at least one minor release that contains the deprecation before the major that removes it. In practice the window should cover the slowest consumer's upgrade cycle. A product that upgrades once a quarter needs a deprecation that stays visible for a quarter, or its team meets the removal without ever seeing the warning. Atlassian publicly commits to a clearly announced deprecation period for every generally available feature, and writing the window down matters more than its exact length.
It needs the same rules, not necessarily the same number. Nathan Curtis noted as early as 2017 that system teams were applying SemVer to their design libraries, not only to code. A renamed component or variant property in a Figma library breaks every file that uses it, the same way a renamed prop breaks code. Keeping the design and code numbers aligned helps designers and engineers talk about one release; keeping them independent works as long as the changelog maps one to the other.
Related services
Related articles
Design
BusinessConsumer cloud app developer: 10 checks before you sign
Ten checks before you hire a studio for a consumer cloud app: sync model, store billing while the US rate sits at zero, Texas age signals, cost per user.
Web