Design component APIs for longevity — composition patterns that minimize breaking changes, codemods for automated migration, and communication strategies for breaking change releases.
Design component APIs for longevity — composition patterns that minimize breaking changes, codemods for automated migration, and communication strategies for breaking change releases.
The best way to handle breaking changes is to avoid creating them. API patterns that age well share common traits: they use composition over configuration, accept open-ended slot props instead of fixed content props, and follow HTML standards for familiar prop names. Compare two Button API designs:
1// Brittle API: configuration-heavy, tightly coupled
2<Button
3 leftIcon="plus"
4 rightIcon="arrow"
5 iconSize={20}
6 iconColor="blue"
7 tooltipText="Add item"
8 tooltipPlacement="top"
9>
10 Add
11</Button>
12
13// Composable API: extensible without new props
14<Button>
15 <PlusIcon aria-hidden size={20} />
16 Add item
17 <ArrowIcon aria-hidden />
18</Button>
19
20// Or with explicit slots for controlled positioning:
21<Button
22 slotStart={<PlusIcon aria-hidden />}
23 slotEnd={<ArrowIcon aria-hidden />}
24>
25 Add item
26</Button>Slot-based APIs let consuming teams customize content without the design system adding new props for every combination. Additions (new slot props) are minor changes; removals are breaking. Structure APIs to grow through addition, not through breaking removal.
A codemod is an automated code transformation script that migrates breaking API changes across thousands of files without manual effort. Use jscodeshift, the de facto standard for JavaScript/TypeScript codemods:
1// codemods/button-kind-to-variant.ts
2import type { Transform, JSXAttribute } from 'jscodeshift';
3
4const transform: Transform = (file, api) => {
5 const j = api.jscodeshift;
6 const root = j(file.source);
7 let changed = false;
8
9 // Find all JSX elements named 'Button' with a 'kind' prop
10 root
11 .find(j.JSXOpeningElement, { name: { name: 'Button' } })
12 .forEach(path => {
13 path.node.attributes.forEach(attr => {
14 if (
15 attr.type === 'JSXAttribute' &&
16 (attr as JSXAttribute).name.name === 'kind'
17 ) {
18 (attr as JSXAttribute).name.name = 'variant';
19 changed = true;
20 }
21 });
22 });
23
24 return changed ? root.toSource({ quote: 'single' }) : file.source;
25};
26
27export default transform;
28
29// Run the codemod:
30// npx jscodeshift --transform=codemods/button-kind-to-variant.ts \
31// --parser=tsx ./src --extensions=tsx,tsTest codemods with a fixtures directory containing before/after file pairs, run with jest-codemods or jscodeshift's built-in test utilities. Publish codemods in an @acme/codemods package versioned to match the design system major.
Not all components are equally risky. Score APIs by their breaking change probability to prioritize stability investment:
1// scripts/api-stability-score.ts
2interface ComponentScore {
3 name: string;
4 propCount: number;
5 changeFrequency: number; // PRs touching this component per quarter
6 usageCount: number; // Import count across all consuming apps
7 riskScore: number; // changeFrequency * usageCount / 100
8}
9
10// Components with riskScore > 10 get API freeze reviews before new props
11// Components with riskScore > 20 require RFC for any prop addition
12const RISK_THRESHOLDS = {
13 standard: 10,
14 requireRFC: 20,
15 freeze: 30, // No new props without design system team approval
16};Typically Button, Input, and Modal score highest because they are used everywhere and change frequently. Put these components through extra API review. Lower-risk components (specialized charts, one-off layout components) can accept faster iteration. Publish the stability scorecard in your design system docs so consumers know which components to depend on for stability.
Breaking changes communicated poorly become adoption blockers. Use a multi-channel approach with a predictable timeline: 12 weeks before the major release, publish an RFC with the complete API change list and draft migration guide. At 8 weeks, host a live Q&A office hours session and post recording. At 4 weeks, enable the deprecation warnings in a minor release and send a team newsletter. At 2 weeks, publish the final migration guide and open a migration help channel. On release day, post the changelog with links to all migration resources and send a calendar reminder for the support window end date.
For especially high-impact changes (renaming a token used by 50+ components), offer white-glove migration support: a design system team member reviews each consuming team's PR that runs the codemod and fixes edge cases. This costs 2 days of engineering time but prevents weeks of frustrated debugging and maintains trust in the system.
When a breaking change requires a long migration window (multiple months), feature flags let teams opt into the new API incrementally:
1// DesignSystemProvider with opt-in to new APIs
2interface DesignSystemConfig {
3 features: {
4 /** Use new slot-based Button API (replaces kind prop in v7) */
5 buttonSlotApi?: boolean;
6 /** Use CSS layers for style isolation (requires CSS @layer support) */
7 cssLayers?: boolean;
8 };
9}
10
11export function DesignSystemProvider({
12 config,
13 children,
14}: {
15 config?: DesignSystemConfig;
16 children: React.ReactNode;
17}) {
18 return (
19 <DesignSystemContext.Provider value={config ?? {}}>
20 {children}
21 </DesignSystemContext.Provider>
22 );
23}
24
25// In Button component:
26function Button({ kind, variant: variantProp, ...props }: ButtonProps) {
27 const { features } = useDesignSystem();
28 // New API only available when feature flag enabled
29 if (features?.buttonSlotApi) {
30 return <NewButton {...props} />;
31 }
32 // Legacy API — warn if using deprecated props
33 useDeprecatedProp(kind, 'kind', 'variant', 'https://ds.acme.com/v7');
34 return <LegacyButton kind={kind} variant={variantProp} {...props} />;
35}Remove feature flags in the next major release. Never let feature flags accumulate across more than two major versions — they become permanent dead weight.
Calculate the API stability score for your top 5 most-used components. For the highest-scoring component, write a jscodeshift codemod that renames the most controversial prop, and test it against a fixtures directory.
Your progress is saved in this browser, on this device only.