EmpireUI
Get Pro
← Blog8 min read#design system#versioning#semver

Design System Versioning: Semantic Versions, Changelogs and Breaking Changes

Semver, changelogs, and breaking changes explained for design systems — what actually matters when you're shipping components to a team.

Code editor showing version numbers and changelog entries for a design system

Why Versioning a Design System Is Different From Regular NPM Packages

Most semver guides assume you're shipping a utility library — something like a date formatter or a validation helper. Design systems are weirder. You're shipping visual contracts. When you change a button's padding from 12px to 16px, that's not a bug fix. It's a layout shift that will silently break pixel-perfect designs in five different apps.

That said, the core semver rules still apply: major versions break things, minor versions add things, patches fix things. What changes is what counts as "breaking." A renamed CSS variable that was undocumented? Major. A new optional prop on a Card component? Minor. A typo fix in an aria-label? Patch. The trick is agreeing on those definitions before version 1.0.0 ships.

Honestly, the biggest mistake teams make is versioning their design system like it's a backend API. Backend devs lean toward "nothing is a breaking change unless it crashes." Frontend teams need stricter rules because your consumers are design-sensitive. Even changing a font-weight from 500 to 600 can break a carefully tuned brand hierarchy.

One more thing — if you're pre-1.0.0, you can technically do whatever you want with the minor and patch slots. But that freedom is a trap. Teams that ship 0.x versions and treat them as consequence-free end up with consumers on 0.14.3 who are terrified to upgrade because they have no idea what changed.

Semantic Versioning Rules That Actually Work for UI Components

Let's get concrete. In a design system, a major version bump (e.g., 2.0.03.0.0) should fire when: a component API removes or renames a required prop, a CSS class name that consumers target changes, a color token is removed or remapped, or a peer dependency version floor rises (like bumping from React 17 to React 18 minimum).

Minor bumps — 2.3.02.4.0 — cover new components, new optional props, new design tokens, and new variants. You're adding surface area. Consumers can ignore the new stuff entirely and nothing breaks. Patch bumps fix rendering bugs, accessibility issues, RTL layout glitches, and documentation errors. Nothing consumers rely on changes shape.

In practice, the rule that saves the most headaches is: if you have to grep for usages across your mono-repo to check whether something is safe to remove, it's a breaking change. Full stop. Don't try to argue that "it was only internal" — if it was exported and typed, it was public.

Worth noting: TypeScript makes this much more auditable. When you delete a prop, the TypeScript compiler screams at every consumer the moment they update. This is a feature, not a bug. Shipping a major version without type errors is a sign you probably didn't actually change anything breaking — or that you're not checking.

Quick aside: tools like changesets automate most of this. You write a tiny markdown file per PR that says "major / minor / patch and here's why," and the tooling handles bumping and changelog generation. Adopted this pattern in 2024 and haven't manually edited a package.json version since.

Writing a Changelog Humans Actually Read

The keep a changelog format is the closest thing to a standard the industry has. Sections like Added, Changed, Deprecated, Removed, Fixed, and Security — in that order. Don't invent a new format because you think yours will be better. It won't. Consistency beats cleverness every time.

Here's a minimal example of what a good design system changelog entry looks like: ``markdown ## [3.2.0] - 2026-08-18 ### Added - Button now accepts a loading prop that shows a spinner and disables pointer events - New --color-surface-overlay token (rgba(0,0,0,0.4) default) ### Changed - Card default border-radius bumped from 8px to 12px (visual only, no API change) ### Fixed - Modal was emitting a focus trap warning in React 18 strict mode (#412) ``

Notice what's in each line: the component name, the prop or token name, and enough context to understand the change without reading the code. You shouldn't have to open a PR to understand what changed. If your changelog entries are one-word summaries like "Button fix" or "styles update," they're useless.

Honestly, the worst changelogs I've seen are auto-generated from git commit messages. Commit messages are for your future self reading git blame. Changelog entries are for the developer three teams over who gets paged at 11pm because something broke after an upgrade. Those are completely different audiences.

If you're building a component library and want to see what solid design system documentation looks like in the wild, check your own CHANGELOG.md against the pattern above. Bet there's at least one section that just says "various improvements." That's not a changelog entry, that's a cop-out.

Handling Breaking Changes Without Alienating Your Consumers

Breaking changes are inevitable. APIs get wrong, tokens get renamed, React versions advance. The question isn't whether you'll ship breaking changes — it's whether you'll do it in a way that doesn't make your consumers dread upgrading.

The gold standard here is a migration guide per major version. Not a long document that covers everything. A focused, step-by-step script that covers the top five most common migration paths. If you renamed colorPrimary to --color-brand-primary in v3, show the exact sed command, the codemod, or the search-and-replace pattern. Don't make people figure it out.

Here's what a useful migration note looks like: ``markdown ## Migrating from v2 to v3 ### Token renames All color tokens are now CSS custom properties prefixed with --empire-. **Before:** `css background: var(--colorPrimary); ` **After:** `css background: var(--empire-color-primary); ` Run this to find all usages: `bash grep -rn 'var(--color' ./src ` ``

That said, the migration guide only helps if people find it. Link it directly from the npm release notes, the GitHub release, and the changelog itself. A migration guide buried in a docs site sub-page nobody visits is only marginally better than no migration guide.

Deprecation cycles are your friend for softer breakages. If you know you're going to remove a prop in v4, mark it @deprecated in JSDoc in v3.0.0, log a console.warn when it's used, and give people at least one minor version of runway. This works especially well for design system tokens where teams need months to migrate across dozens of files.

Automating Version Bumps and Release Pipelines

Doing semver manually is how mistakes happen. Someone forgets to bump, someone bumps the wrong slot, someone publishes without running the build. Automate it. Full stop.

The changesets workflow I mentioned earlier pairs nicely with GitHub Actions. Each PR that touches a component ships a .changeset/*.md file alongside the code. On merge to main, a bot PR appears that aggregates all pending changesets, updates the version, and writes the changelog. You review the bot PR, merge it, and the publish step fires automatically. Here's a barebones action step: ``yaml - name: Create Release Pull Request or Publish uses: changesets/action@v1 with: publish: npm run release version: npm run version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``

Worth noting: even if you're not publishing to npm and your "package" is just an internal monorepo library, changesets still works perfectly. The publish step can push to a private registry or just tag the commit. The changelog discipline is worth it regardless of where the package lives.

One more thing — lock your peer dependency ranges tightly. A peer dep of react: >=16 sounds flexible and friendly, but it means someone can try to use your v3 components with React 16 and get subtle breakages. Be explicit: react: ^18.0.0. The semver caret pins the major. If you need to support multiple React majors simultaneously, that's what the peerDependenciesMeta field is for, and it's worth the extra effort for widely-used systems.

Look, nobody wants to maintain a release pipeline. But a five-minute investment in automation pays back every time you ship. It's the difference between "we should update the design system" being a scary proposition and a routine Monday morning task.

Versioning Design Tokens Alongside Components

Design tokens deserve their own versioning story. It's common to have a separate @your-org/tokens package that components consume. When you change a token value — say, bumping --color-brand-500 from #6366f1 to #4f46e5 — that's potentially a major visual change even if the token name stays the same.

The community hasn't fully settled this yet. Some teams treat value changes as patches ("it's just a bug fix, the old value was wrong"). Others treat any visible color change as a minor version at minimum. My take: if the change was intentional and would update brand perception, call it minor. If it was fixing an off-by-one hex error nobody noticed, patch is fine.

Tools like Style Dictionary help here — you can diff token output files between versions and catch changes that might otherwise slip through. Run a diff in CI between the generated token file in the PR branch and the one on main. If anything changes, the PR has to document why in a changeset file. This caught three silent token regressions on a project in 2025 before they hit production.

If you're building on top of a component library like Empire UI that ships glassmorphism components and style primitives, you'll want to track upstream token changes the same way. Watch the changelog, pin to explicit minor versions in production apps, and use Dependabot or Renovate to notify you rather than auto-merging design system updates. Auto-merging a design system major is how you get a surprise brand refresh in production.

What a Mature Versioning Setup Actually Looks Like

You don't need to build all of this on day one. Most teams get value from just three things early on: a CHANGELOG.md that's actually maintained, explicit semver tagging on releases, and a short migration guide for every major bump. Start there.

As the system matures and consumer count grows, add changesets for automation, add a dedicated tokens package if your token surface is large, add a deprecation cycle policy ("props deprecated in vX will be removed in vX+1"), and add CI checks that fail if a changeset file is missing from a PR that modifies component files. That last check alone prevents the most common versioning debt.

A mature setup also has a way to test across versions. If you have five apps consuming your system, keep a quick integration smoke test that can run against any published version. When someone reports a bug, you can bisect across minor versions in minutes instead of pulling old commits.

The motion design tokens post covers token versioning from a slightly different angle if you're dealing with animation values specifically. And if your design system includes complex CSS work — variable fonts, advanced shadows, the kind of thing you'd build with the box shadow generator — those visual properties deserve the same changelog discipline as your component APIs. Consumers notice when shadows shift.

Versioning a design system well is one of those things that feels like overhead until it saves you from a 2am production rollback. Do it right from the start.

FAQ

What counts as a breaking change in a design system?

Anything that removes, renames, or changes the behavior of a public API — props, tokens, CSS class names, or peer dependency floors. Visual changes that shift layout (like padding or border-radius adjustments) are typically treated as minor, not breaking, unless they alter component dimensions significantly.

Should I version design tokens separately from components?

If your token surface is large or consumed independently, yes — a separate package with its own semver gives teams more granular control. For smaller systems, co-versioning tokens and components in one package is simpler and reduces coordination overhead.

How do I handle deprecating a prop without breaking consumers?

Add a @deprecated JSDoc tag, fire a console.warn when the prop is used, and keep it working for at least one major version cycle before removal. Give consumers a clear replacement in the deprecation message.

Do I need to publish to npm to use semantic versioning?

No. Semver is a convention, not an npm feature. Internal monorepo packages, private registries, and even git tags can all follow semver rules. The changelog and migration guide discipline matters regardless of where your package lives.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

CSS Custom Properties as a Design System: The Right ArchitectureDesign System Documentation: Storybook, Zeroheight and Custom DocsChangesets: Automated Versioning and Changelogs for UI LibrariesMonorepo Design System: Shared Packages, Storybook, Publishing