Changesets: Automated Versioning and Changelogs for UI Libraries
Learn how Changesets automates versioning and changelog generation for React UI libraries — no more manual semver math or forgotten release notes.
Why Manual Versioning Breaks Down Fast
Honestly, manually bumping package versions in a UI library is one of those tasks that seems fine until you're maintaining six packages across a monorepo and someone forgot to update the changelog before cutting the release. Then it's 11pm, your users are filing issues, and you're running git log --oneline trying to reconstruct what changed.
The problem isn't laziness. It's that version management is genuinely tedious — you're tracking patch vs. minor vs. major decisions across multiple contributors, writing CHANGELOG entries by hand, and hoping nobody pushes a breaking change without bumping the major. Humans are bad at this. Tools are not.
Changesets (the @changesets/cli package, currently at v2.27.x) solves this by making versioning a first-class part of your development workflow rather than an afterthought at release time. If you're building or maintaining a component library — even a single-package one — it's worth understanding how it works.
What Changesets Actually Does
At its core, Changesets is a workflow tool. When you make a change that warrants a version bump — say, a new variant prop on your Button component — you run pnpm changeset and answer two questions: which packages are affected, and what type of bump is it (patch, minor, or major)? It writes a small markdown file into a .changesets/ directory and commits it alongside your code.
When you're ready to release, pnpm changeset version reads all the pending changeset files, calculates the correct version for each package, updates your package.json files, and assembles a proper CHANGELOG.md — automatically. Then pnpm changeset publish pushes everything to npm. That's it. No manual semver math, no forgotten CHANGELOG sections.
Where it gets genuinely interesting is in monorepos. If your @empire-ui/core package bumps from v3.1.4 to v3.2.0 and @empire-ui/motion depends on it, Changesets will also bump @empire-ui/motion with a patch to update that dependency reference. The dependency graph is respected automatically.
Setting Up Changesets in a Pnpm Workspace
Setup is about five minutes. Install the CLI, initialize, and you're mostly done:
pnpm add -D @changesets/cli
pnpx changeset initThis creates a .changeset/config.json file. The defaults are reasonable, but there are a few settings worth knowing about. baseBranch should match your main branch name. access defaults to restricted — change it to public if your packages are open source. updateInternalDependencies can be set to patch or minor to control how internal dep bumps cascade across your workspace.
The Day-to-Day Workflow for Library Authors
Once Changesets is set up, the workflow fits naturally into a PR-based process. A contributor makes a change — let's say they add a new glassmorphism variant to a Card component (something we've explored in depth here). Before opening their PR, they run pnpm changeset and describe the change. That changeset file goes into the PR.
Here's what that changeset file looks like in practice:
---
"@empire-ui/core": minor
---
Added `glassmorphism` variant to Card component.
Uses `backdrop-filter: blur(12px)` and `rgba(255,255,255,0.15)` background.
Requires Tailwind v4.0.2 or later for the `backdrop-blur-md` utility to work correctly.When the PR is merged, the changeset file lives in .changeset/. It accumulates with other changesets from other PRs. When you're ready to release — weekly, bi-weekly, whenever — you run pnpm changeset version, review the generated diff, and then publish. The CHANGELOG writes itself.
Automating Releases with GitHub Actions
Running this manually is fine for small teams. For anything with multiple contributors, you'll want the official Changesets GitHub Action, which automates the entire version-and-publish loop. The action opens a "Version Packages" PR automatically whenever there are pending changesets on your main branch. You merge that PR when you want to release.
# .github/workflows/release.yml
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}This workflow does one of two things depending on the state of the repo: if there are pending changesets, it opens or updates a "Version Packages" PR. If that PR is already merged (meaning packages have been versioned but not yet published), it publishes to npm. It's a clean two-step process.
Snapshot Releases and Pre-Release Modes
Here's something a lot of people miss: Changesets has a pre-release mode that lets you cut alpha, beta, or rc versions without consuming your pending changesets. Run pnpm changeset pre enter alpha, commit your changesets as usual, and the version command will produce versions like 3.2.0-alpha.1, 3.2.0-alpha.2, etc. When you're ready for the stable release, run pnpm changeset pre exit.
There's also snapshot releases — pnpm changeset version --snapshot — which produce versions tagged with a timestamp like 0.0.0-snapshot-20261102120000. These are great for CI preview builds where you want to test a real npm install without polluting your version history. Tools like theme-toggle-react often ship snapshot builds for early adopters to test against.
Why does this matter for UI libraries specifically? Because your users often want to try new components before a stable release. Snapshots give them a real package to install (npm install @empire-ui/core@0.0.0-snapshot-20261102120000) without you having to commit to a version number yet. It's one of those features that sounds niche until you actually need it.
Changesets vs Semantic Release vs Manual Bumping
Semantic Release and Changesets solve the same problem differently. Semantic Release derives version bumps from commit message conventions (Conventional Commits) — a feat: commit triggers a minor bump, fix: triggers a patch, BREAKING CHANGE in the footer triggers a major. It's fully automated and requires no developer intervention after setup.
Changesets takes a more intentional approach. You decide what type of bump each change warrants, not an algorithm parsing your commit messages. This matters for UI libraries, where a commit that's mechanically a feat: (new utility class, new CSS custom property) might not actually warrant a minor bump because it's purely additive and invisible to users. Developers working on Tailwind shadow utilities or gradient tools know how easy it is to add a property that seems minor but technically changes the API surface.
Manual bumping — editing package.json by hand, writing CHANGELOG entries manually — doesn't scale. Even for a single package, you'll forget entries, miscategorize bumps, or skip the CHANGELOG entirely during a busy week. The question isn't whether to automate, it's which tool fits your team's workflow better. For most UI library maintainers, Changesets wins because it keeps humans in the loop on the semantic decisions while automating the mechanical parts.
Practical Tips for UI Component Library Workflows
A few things that save headaches in practice. First, enforce changeset presence in CI. Add a step that runs pnpm changeset status --since=origin/main and fails if no changesets are present. This catches PRs where contributors forgot to add one. You can exempt documentation-only PRs with a skip-changeset label.
Second, write changeset descriptions that are actually useful. The generated CHANGELOG.md is only as good as the descriptions contributors write. Make it a code review requirement. "Fixed bug" is not a changeset description. "Fixed incorrect border-radius on Card when size='sm' is set — was 4px, now correctly matches the 8px gap in the design spec" is a changeset description. Your CHANGELOG consumers will thank you.
Third, if you're building a library that ships multiple visual styles — like a gradient generator or shadow tools — consider splitting your package into logical sub-packages early. Changesets handles monorepos well and it's much easier to set up that structure before you have users depending on a single monolithic package than to split it later.
FAQ
No. Changesets works perfectly well with a single package. The monorepo features are there when you need them, but the core workflow — writing changeset files, running changeset version, publishing — is the same regardless of how many packages you have.
Changesets takes the highest bump type. If one changeset says patch and another says minor for the same package, the version command will apply a minor bump. The logic is safe — it can't accidentally downgrade what a contributor intended.
Yes. Changesets is a workspace-agnostic tool. It works with npm workspaces, yarn workspaces (classic and berry), and pnpm workspaces. The CLI commands are identical; only the package manager prefix changes (npm run changeset vs pnpm changeset).
When you run pnpm changeset, select all affected packages and mark each as major. Changesets will handle the cascade — if package B depends on package A and A gets a major bump, B will also get at minimum a patch bump to update its dependency reference.
It's behaving correctly — that PR is how you control when packages are actually published. You merge the "Version Packages" PR when you want a release. If you don't want releases on every push, that's fine; just leave the PR open until you're ready. The action will update it as new changesets accumulate.
Yes. The changesets/action and the changeset status command both support a --since flag. You can also use the changeset-bot GitHub app, which adds a comment to PRs reminding contributors to add a changeset and lets you bypass the check with a label like no-changeset-needed.