Build a structured deprecation workflow that removes technical debt from your design system without breaking consuming teams — runtime warnings, migration guides, and automated cleanup.
Build a structured deprecation workflow that removes technical debt from your design system without breaking consuming teams — runtime warnings, migration guides, and automated cleanup.
A predictable deprecation lifecycle reduces friction and maintains trust. Phase 1 — Announce (minor release, Month 0): ship the new API alongside the old one, add runtime warnings, publish migration guide, communicate in the design system newsletter and Slack channel. Phase 2 — Migrate (support window, Months 1–3): provide office hours support, track adoption of new API via codebase scanning, run codemods in repos that have not migrated by Month 2. Phase 3 — Remove (next major release, Month 4+): remove deprecated API, update all internal references, publish final removal changelog entry with link to migration guide archived at a permanent URL. Never remove without completing Phase 2 scanning — knowing the impact before removal prevents emergency rollbacks. If any critical consuming repo has not migrated, delay Phase 3 until it does.
Deprecation warnings should be visible in the browser console during development but invisible in production to avoid noise for end users:
1// packages/react/src/utils/deprecate.ts
2const warned = new Set<string>();
3
4export function deprecate(message: string, url: string): void {
5 if (process.env.NODE_ENV === 'production') return;
6
7 // Warn once per unique message per session to avoid console flooding
8 const key = `ds:deprecation:${message}`;
9 if (warned.has(key)) return;
10 warned.add(key);
11
12 console.warn(
13 `%c[DesignSystem Deprecation]%c ${message}\n` +
14 `Migration guide: ${url}`,
15 'color: #f59e0b; font-weight: bold;',
16 'color: inherit;'
17 );
18}
19
20// Usage in deprecated component
21export function OldCard(props: OldCardProps) {
22 deprecate(
23 "'OldCard' is deprecated. Use 'Card' from @acme/react instead.",
24 'https://ds.acme.com/migration/v7#old-card'
25 );
26 // Render using the new Card internally for gradual migration
27 return <Card {...mapOldPropsToNew(props)} />;
28}The Set-based deduplication ensures each warning appears exactly once per session, keeping the console clean during development sessions that render the same component many times.
Knowing which teams use deprecated APIs is essential for Phase 2 planning. Build automated scanning:
1// scripts/scan-deprecations.ts
2import { glob } from 'glob';
3import { readFileSync } from 'fs';
4
5const DEPRECATED_PATTERNS = [
6 { pattern: /from '@acme\/react.*OldCard'/, api: 'OldCard', replacement: 'Card' },
7 { pattern: /from '@acme\/react.*useOldHook'/, api: 'useOldHook', replacement: 'useNewHook' },
8 { pattern: /kind=["']/, api: 'Button kind prop', replacement: 'Button variant prop' },
9];
10
11async function scanForDeprecations(rootDir: string) {
12 const files = await glob('**/*.{tsx,ts,js}', {
13 cwd: rootDir,
14 ignore: ['node_modules/**', 'dist/**', '*.test.*'],
15 });
16
17 const report: Record<string, { count: number; files: string[] }> = {};
18
19 for (const file of files) {
20 const content = readFileSync(`${rootDir}/${file}`, 'utf8');
21 for (const { pattern, api } of DEPRECATED_PATTERNS) {
22 if (pattern.test(content)) {
23 if (!report[api]) report[api] = { count: 0, files: [] };
24 report[api].count++;
25 report[api].files.push(file);
26 }
27 }
28 }
29
30 return report;
31}
32
33// Run and post to Slack or GitHub issue
34const report = await scanForDeprecations(process.argv[2] ?? '.');
35console.table(Object.entries(report).map(([api, data]) => ({
36 api, count: data.count, affectedFiles: data.files.length
37})));ESLint rules catch deprecated usage at the editor and CI level, before runtime warnings are ever seen:
1// packages/eslint-plugin-ds/src/rules/no-deprecated-imports.ts
2import type { Rule } from 'eslint';
3
4const DEPRECATED_IMPORTS: Record<string, { replacement: string; since: string; until: string }> = {
5 OldCard: { replacement: 'Card', since: 'v6.2.0', until: 'v7.0.0' },
6 OldButton: { replacement: 'Button', since: 'v6.0.0', until: 'v7.0.0' },
7};
8
9export const noDeprecatedImports: Rule.RuleModule = {
10 meta: {
11 type: 'suggestion',
12 messages: {
13 deprecated:
14 "'{{ name }}' is deprecated since {{ since }} and will be removed in {{ until }}. " +
15 "Use '{{ replacement }}' instead. See migration guide in DS docs.",
16 },
17 schema: [],
18 },
19 create(context) {
20 return {
21 ImportDeclaration(node) {
22 if (!node.source.value.toString().includes('@acme/react')) return;
23 node.specifiers.forEach(specifier => {
24 if (specifier.type !== 'ImportSpecifier') return;
25 const name = specifier.imported.name;
26 const info = DEPRECATED_IMPORTS[name];
27 if (info) {
28 context.report({
29 node: specifier,
30 messageId: 'deprecated',
31 data: { name, ...info },
32 });
33 }
34 });
35 },
36 };
37 },
38};Publish this as @acme/eslint-plugin-ds and add it to the recommended ESLint config that consuming apps extend. Violations appear as warnings (not errors) during the migration window, becoming errors after the removal.
Communication is the difference between a smooth deprecation and a team revolt. Prepare a template for each deprecation announcement:
1# Deprecation Notice: OldCard Component
2
3**Deprecated since:** v6.2.0 (released 2026-03-01)
4**Removal date:** v7.0.0 (planned 2026-06-01)
5**Impact:** 23 files across 6 product repos
6
7## What is changing
8`OldCard` is replaced by `Card` which provides a more flexible
9slot-based API and full dark mode support.
10
11## Migration (automated)
12Run the codemod to migrate automatically:npx @acme/codemods@latest old-card-to-card ./src
1
2## Migration (manual)+ import { Card } from '@acme/react';
+ <Card>
+ <Card.Header>Hello</Card.Header>
+ <Card.Body>Content</Card.Body>
+ </Card>
1
2## Support
3- Office hours: Thursdays 2–3pm (link in #design-system Slack)
4- Slack channel: #design-system-migration
5- Full guide: https://ds.acme.com/migration/v7#old-cardPost this in the design system changelog, email newsletter, and team Slack channel simultaneously. Pin in Slack for the full deprecation window.
Identify the oldest deprecated API still in your design system. Run a scan across your product repos to find all usages. Write the deprecation announcement and set a removal date for the next major version.
Your progress is saved in this browser, on this device only.