A new component has a demo. An old component has consumers.
That difference is easy to underestimate. The replacement might have a cleaner API, better composition, and more consistent tokens. None of that tells a product team how to migrate the screen they need to ship this week.
I think deprecation deserves the same design effort as the component itself. Not because every old API needs to survive forever, but because removing it safely is part of the product the design-system team provides.
The useful question is not just “What replaces this?” It is “What evidence would let us remove this without surprising the people who depend on it?”
Deprecation is a migration contract
Consider an illustrative React API:
This looks like a search-and-replace. It might be. But what happens when a wrapper passes primary through a spread? What happens when both props are present? Does the default variant stay the same? Does the rendered element still have the same button type?
Those are contract questions, not cleanup details.
Before marking the old prop deprecated, I would write down the replacement, supported compatibility period, owner, known exceptions, and removal criteria. An illustrative migration record could be this small:
A calendar date makes the work schedulable. It does not prove the work is finished. If a known consumer is blocked, either resolve that blocker or explicitly revise the plan. Quietly extending “temporary” support is how an undocumented second API becomes permanent.
For a stable package following Semantic Versioning, marking public functionality deprecated belongs in a minor release; an incompatible removal requires a major release. That is release signalling, not a migration service. A major version number cannot explain which replacement preserves a consumer's behavior. Semantic Versioning
Count consumers, not just imports
A repository search is a good first pass. It is not a complete inventory.
Direct imports miss re-exports, local wrappers, generated code, and applications outside the repository. A token can be consumed through a CSS variable, a design-tool library, or a platform-specific output without appearing as a TypeScript import at all.
I would combine static searches with package-version inventories, wrapper ownership, and a short list of consuming applications. Each entry should have a responsible team and a migration state: not started, in progress, blocked, or verified.
Build-time checks can help stop new usage while existing consumers migrate. If runtime diagnostics are useful, keep them development-only and actionable. A warning should name the deprecated API, show the replacement, and point to the guide. Repeated console noise is not adoption work, and production telemetry should not collect customer data just to count a component.
Most importantly, record the limits of the inventory. “No usages found in these repositories” is a defensible statement. “Nobody uses this” usually needs more evidence.
Components and tokens fail differently
A component migration can change behavior while preserving its screenshot. A token migration can preserve today's colour while changing its meaning.
Suppose an old token called brand.blue is used for links, focus rings, and decorative backgrounds. Replacing every occurrence with color.text.link would give three different decisions one semantic name. The values might match in the current theme and diverge later.
First classify the usages by purpose. Then migrate them to the appropriate semantic tokens. If a compatibility alias is genuinely equivalent, keep it as a bridge with a documented owner and removal plan. Do not silently point an ambiguous token at a narrower meaning and call the migration complete.
The Design Tokens Community Group format supports references and deprecation metadata. Those mechanisms let tools express relationships and warnings; they do not establish that two tokens mean the same thing. That remains a design and engineering decision. Design Tokens format
For components, make the behavioral checklist explicit: accessible name, keyboard interaction, focus placement and return, disabled behavior, form submission, and controlled versus uncontrolled state. Test applicable cases in a real consumer, not only an isolated story. A renamed dialog API can compile while moving focus to the wrong place. The W3C modal dialog pattern is a useful reference for the focus and keyboard behavior to check, including exceptions to returning focus to the invoker.
Automate the mechanical part, expose the rest
Codemods are valuable when a transformation is predictable. They are less helpful when they confidently rewrite a case they do not understand.
For the Button example, an automated transform might handle a direct primary prop. It should leave a report for spread props, wrapper APIs, or conflicting primary and variant values rather than guessing. The migration guide should explain how to resolve those cases.
This is not an unusual limitation. MUI's migration documentation calls out cases where codemods cannot cover all patterns, including prop spreading and cross-file dependencies. Treat the generated diff as a starting point for verification, not evidence that the migration is correct. MUI migration guidance
Run the transform on representative consumers first. Review the diff, run type checks and behavior tests, and compare the relevant visual states. Check that running it again does not keep changing already-migrated code. Then publish the tool with its supported cases, limitations, and a way to report blockers.
AI can help classify usages or prepare migration diffs too. I would give it the same boundaries: no invented replacements, no silent changes to product behavior, and an explicit list of cases that need human judgment.
Make the removal boring
A useful lifecycle is:
- Introduce the replacement and prove it handles representative consumers.
- Document the old API as deprecated, with a usable migration path.
- Prevent new deprecated usage without unexpectedly breaking existing builds.
- Migrate known consumers and resolve exceptions with their owners.
- Remove the old API in the appropriate release, with release notes and a recovery plan.
- Retire the aliases, warnings, examples, and compatibility tests that no longer belong.
Keep a small set of signals: remaining usages in the known inventory, blocked consumers, old package versions still supported, and the next review date. A percentage migrated can look reassuring while the last remaining usage sits in the most important workflow.
Backward compatibility can still be the right decision. If an old API is inexpensive to maintain and migration would impose disproportionate cost, preserving it deliberately is reasonable. The problem is not that compatibility exists. The problem is when nobody can explain why it exists or who owns it.
This also keeps governance practical. The design-system team supplies the contract, tooling, and support. Product teams own their consumer changes. Exceptions have a reason and a review point instead of becoming permanent permission to ignore the system.
A removal-readiness checklist
Before removing a component, prop, or token, I would want clear answers to six questions:
- Can consumers find the replacement and understand the behavior that changes?
- Do we know which consumers we checked and which remain outside our visibility?
- Are automated changes separated from cases that require judgment?
- Have representative interactions, themes, and accessibility behavior been verified?
- Are remaining exceptions owned, and does the release plan account for them?
- Can a team recover if the migration reveals a problem after release?
A design system should make the next good decision easier. That includes helping teams stop depending on a decision that no longer fits.
Adding components demonstrates capability. Retiring them safely demonstrates that the system can evolve.
