Skip to content
Design

Design 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.

November 4, 2026 · 9 min read

A row of release marks across the top, small grey dots and squares and one large white tile; below, five lanes run right from five tiles on the left, and at the column of the large tile every lane is cut: two cross the gap on small white squares and run on in white, three end at grey rings

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?".

  1. Component and prop names. The import path, the export name, every prop and the values it accepts.
  2. Prop types. In TypeScript the type is the contract. A union of three strings promises that a fourth will never be needed.
  3. Rendered structure. Which element is the root, which element receives spread props, how children are laid out.
  4. Styling hooks. Class names, CSS custom properties and data attributes a consumer can target.
  5. Tokens. Names first, then values. A renamed token breaks every stylesheet that references it.
  6. Accessibility semantics. Roles, landmarks, labels and focus behaviour, which page-level audits read.
  7. 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.

ChangeBumpWhat the consumer sees
A component or prop is removedMajorBuild or type error on upgrade
A prop's type is narrowed (any string becomes three allowed strings)MajorType errors on values that used to work
A dependency moves to a new major versionMajorPeer conflicts or two copies of one package
Spread props move to a different elementPotentially majorHandlers and layout props land on the wrong node
The children container changes display, block to flexPotentially majorWhitespace collapses, children reflow
A component stops reading a CSS custom property consumers setPotentially majorOverrides stop working, with no error
A component adds or removes a main or contentinfo landmarkMajorDuplicate or missing landmark in axe audits
A data-component attribute is renamed or removedMajorTests 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.

  1. 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.
  2. 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).
  3. Ship a codemod for the mechanical part. MUI's v6 upgrade rewrites the old Grid props into the new size and offset shape 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).
  4. 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.
  5. 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

Frequently asked questions

Related articles

Studio

Start a project.

We write about what we build. Tell us what you want to build.