Alcott

Loading site
Skip to content

2026-09-25 · 7 min · By Alcott Dube

Component library versioning: how to avoid breaking teams

I treat versioning as an agreement with consuming teams, with explicit breaking changes, dated deprecations, useful release notes, and migration work that can be estimated.

Three related ceramic structures connected by transitional pieces, with a terracotta connector spanning a gap.

I version a component library by reserving major releases for changes that require consumer action, introducing replacements before removing old behaviour, and giving deprecations a clear support window. I pair each release with actionable notes, a tested migration path, and checks against consuming applications before widening adoption.

How to classify breaking component changes

I start component library versioning with a written compatibility contract. For a stable library, a patch corrects behaviour within that contract, a minor adds compatible capability, and a major changes something consumers must accommodate. The contract covers properties, events, tokens, styles, keyboard interaction, and supported environments. Exported names alone aren't enough. A component can still compile while breaking a checkout layout or changing where focus lands.

Consider a hypothetical dialog update. Adding an optional size property is usually a minor change. Removing an existing size value is a major change. Changing the default width needs judgement: if supported layouts now overflow, calling it a visual fix doesn't make it compatible. I review screenshots and interaction tests before deciding. I also distinguish documented guarantees from internal details, so teams know which customisations they own.

Carbon's migration guide provides a reference for treating a major upgrade as explicit consumer work. I use that distinction in release review: can an application take this update without editing supported usage? If not, I either preserve the old behaviour temporarily or schedule a major release. I don't use version numbers to disguise the cost.

How to set a component deprecation policy

I deprecate a component only after its replacement is usable. That means documented properties, accessibility checks, examples, and a known migration route. A warning that points to an unfinished replacement transfers uncertainty to product teams. For each deprecation, I record the replacement, the reason, the first deprecated version, the earliest removal version, and the person responsible for answering migration questions. Those details belong beside the component documentation.

For a library serving several independently scheduled teams, I might start with a policy of at least 90 days and removal only in a major release. That's a proposed operating rule, not an industry benchmark. If a consuming application ships quarterly, 90 days may provide just one realistic upgrade opportunity. I agree the window with those teams before announcing it, rather than treating silence as acceptance.

During that window, I keep the old component installable and document which fixes it still receives. I prefer editor annotations and development-only warnings, emitted once per deprecated usage category rather than on every render. Critical security or accessibility defects may need a shorter timetable. I state that exception explicitly, identify affected applications, and arrange a fix instead of quietly changing the policy.

Two rows of modular shapes linked by amber adapters, with one irregular piece set aside.
The adapters represent temporary compatibility while individual components move to their replacements.

What to include in a component library changelog

I write changelogs for the person deciding whether to schedule an upgrade, not the person reviewing the original code change. Each entry names the affected component, describes the observable difference, and states whether action is required. 'Refactored selection handling' tells a consuming team almost nothing. 'Controlled selection now requires an explicit change handler' identifies a decision they can check against their implementation.

The Polaris React release history is a useful reference for version-specific release records. I keep that record separate from the full migration guide, but link the two wherever consumer work is required. For a hypothetical removal, I'd expect an entry with these details:

  • Affected usage: Dialog instances using the deprecated width property.
  • Required action: replace width with size, using the documented value mapping.
  • Compatibility: the old property remains supported throughout version 4 and is removed in version 5.
  • Verification: check narrow viewports, long headings, and any content with a minimum width.
  • Migration help: link to before-and-after examples, known exceptions, and an estimated effort range based on the published example.

How to write a component migration guide

I organise migration guides around changes a team must make, rather than the library's internal architecture. I list prerequisites first: supported runtime, framework version, package dependencies, and any required stylesheet changes. Then I show the smallest before-and-after example for each breaking change. Carbon's dedicated migration documentation is the model I take from here: upgrading deserves its own instructions, not a paragraph buried in release notes.

For a property rename across 200 call sites, a codemod can remove repetitive editing. It cannot establish that the new component behaves correctly in every context. I document exactly what the script changes, what it skips, and how to inspect its output. I test it against direct imports, aliased imports, and local wrappers. If it cannot interpret a wrapper safely, I want a reported exception, not a guessed edit.

I split the remaining work into mechanical changes and decisions requiring a person. Replacing an import path is mechanical. Choosing a new validation pattern isn't. For larger migrations, I provide a temporary adapter where the old behaviour can be preserved honestly. I give the adapter its own removal condition; otherwise, it becomes a second component library that nobody planned to maintain.

How to test and roll out a component library release

I publish a prerelease before a major version and ask a small, representative set of consuming applications to install it explicitly. A documentation site isn't enough. I want one application with dense forms, one with substantial theme customisation, and one with complex keyboard interaction, where those patterns exist. The point is to expose different dependencies on the library, not to collect three approvals from nearly identical implementations.

I check compilation, interaction tests, accessibility tests, and visual differences separately. A passing build says little about a clipped menu. Screenshot comparisons say little about focus returning to the wrong control. I require someone to review intended visual changes rather than automatically accepting new baselines. I also check dependency resolution, because an upgrade that installs two incompatible copies of a styling dependency can fail far from the changed component.

My rollout has a pause point after the first production adopter. I agree the observation period around that application's traffic and release cadence, then expand adoption if its checks hold. Teams keep their dependency lockfiles and previous package versions available. I test reversion too, especially when a migration changes application code that the previous library version cannot understand.

How to track adoption and remove deprecated components

I track adoption by application and installed package version, then count remaining deprecated usages where source access allows it. Those measures answer different questions. An application can install the latest minor release while still relying on every deprecated property. A repository scan also needs to account for wrappers and generated code. I record blind spots rather than presenting an incomplete count as proof that removal is safe.

My working register includes the application owner, target release, remaining migration tasks, and blockers. I measure elapsed migration time and manual interventions during the pilot, then use that evidence to revise estimates for other teams. Download counts aren't a reliable substitute for this register: automated builds and repeated installs can inflate them without representing another migrated application. I skip a polished adoption dashboard until the underlying records are dependable.

Before removal, I check the promised window, unresolved blockers, and support obligations. If one low-activity application remains, the choice might be a documented older release line rather than delaying everyone else. That line needs an owner and a defined fix policy. Once removal ships, I retain version-specific documentation and migration links, so a team upgrading six months later can still reconstruct the changes it needs.

Questions people ask

Does every visual change need a major version?

No. I treat a visual change as breaking when it violates a supported expectation or requires application changes. A correction within the documented contract may be a patch, but I still flag visible differences.

How long should a component stay deprecated?

I base the window on consuming teams' release cycles and migration effort. Ninety days can be a starting policy, but I extend it when that doesn't provide a realistic upgrade opportunity.

Should every component have its own version?

Not necessarily. I prefer one version for a tightly coupled package. Separate versions make sense when components genuinely ship independently and the team can test and explain their supported dependency combinations.

Can a codemod handle a component library upgrade?

It can handle predictable edits, such as import changes and some property renames. I still require manual review for behaviour, layout, accessibility, and any usage the script cannot interpret safely.

Where I checked my thinking

Start a project