Design · November 3, 2026
Design System Migrations Need Reversible Components, Not Rewrite Plans
By Anika Sarder · Digital Marketing Specialist
Photo by Florian Olivo on Unsplash
A migration is safe only when one component can fail by itself
A design-system migration should not be managed as a single rewrite. It should be managed as a sequence of reversible component changes, each protected by a compatibility layer, a visual check, and a production rollout. The reason is simple: a system-wide styling abstraction has too many consumers for a clean cutover to be credible.
GitHub’s migration away from CSS-in-JS makes the case unusually concrete. The Primer team moved components to CSS Modules, feature-flagged the new implementation, compared visual-regression snapshots, and rolled changes out gradually. GitHub reports that the completed Primer migration reduced server-side rendering time by 55% and component initialization time by 25% (GitHub Engineering). The important lesson is not “CSS Modules are better.” It is that the migration was designed to produce evidence before it demanded trust.
That distinction matters for any large design-system change: replacing a styling runtime, introducing native CSS, changing theme infrastructure, or moving from one component API to another. A migration plan that says “rewrite all consumers, then switch” creates a single irreversible event. A migration plan that says “make each component switchable, prove equivalence, then remove the bridge” turns architecture work into a controlled series of experiments.
Why rewrite plans fail at the design-system boundary
A rewrite treats the design system as a package. Production treats it as a graph of components, overrides, themes, tests, build assumptions, and undocumented consumer behavior.
The difference is visible in the failure modes. A team can update the Button implementation and still break a product because a consumer depends on an undocumented class, an sx escape hatch, a theme variable that was never listed in the API, or a selector that leaked across component boundaries. The design-system repository may compile while a downstream application changes in ways its unit tests do not describe.
MDN reached a similar conclusion from a different direction. Its frontend rebuild had accumulated Sass, CSS variables, entangled selectors, a large render-blocking stylesheet, React boundaries around static content, and duplicate implementations for interactive pieces. The new architecture used web components for embedded interactivity and a more direct assembly model. MDN describes the result as a simpler development environment with a single command and a production architecture closer to local development (MDN’s frontend deep dive).
These are not identical migrations, but they expose the same architectural rule: the unit of change must match the unit that can be observed. If the team can only observe a whole application after a global rewrite, it cannot isolate causality. If it can observe one component behind a switch, it can tell whether a regression came from style translation, theming, layout, hydration, or a consumer assumption.
The migration target should therefore be expressed as a component contract, not a technology replacement:
| Contract to preserve | Evidence to collect before switching | Typical failure hidden by a rewrite |
|---|---|---|
| Visual output | Automated snapshots plus representative pages | Spacing, wrapping, and state drift |
| Runtime behavior | Interaction tests and keyboard paths | Focus loss, broken menus, modal regressions |
| Theme semantics | Light, dark, and high-contrast fixtures | Hard-coded colors or missing tokens |
| Consumer escape hatches | Inventory of overrides and custom props | “Compatible” component that rejects real usage |
| Delivery performance | Server-render and initialization measurements | Faster CSS offset by slower application work |
The table is deliberately broader than a screenshot comparison. Visual parity is necessary, but a design-system migration can preserve pixels while breaking accessibility, interaction state, or theme behavior.
GitHub’s playbook: bridge first, delete last
GitHub’s most useful decision was to preserve the old consumer behavior while changing the implementation underneath. During the Primer migration, the team created a wrapper package, @primer/styled-react, so components could move to the new implementation while consumers using the sx prop continued to work. Consumers that no longer needed the prop could import the direct @primer/react version and receive the new path without waiting for the whole codebase to migrate (GitHub Engineering).
That wrapper is not architectural failure. It is a migration instrument. The mistake is allowing a compatibility layer to become permanent and invisible. A bridge should have an owner, an exit condition, and a metric that shows whether traffic is leaving it.
A practical component migration has four states:
legacy implementation
│
├── compatibility wrapper ──> new implementation
│ │
│ └── usage + error telemetry
│
└── rollback switch
The switch does not need to be sophisticated. It can be a feature flag, an import boundary, a route-level configuration value, or a build-time toggle. Its job is to make the new path reversible while evidence accumulates. GitHub used feature flags to compare and roll out changes to its team, staff, and then all users. That sequence converted a risky architectural move into a production feedback loop.
The same pattern applies beyond styling:
- Keep the public component contract stable. Translate the implementation before changing names or props.
- Put new behavior behind a narrow switch. The switch should select a component or package, not fork business logic throughout the application.
- Compare both paths on the same fixtures. Use the same content, viewport, theme, locale, and interaction sequence.
- Roll out by consumer cohort. Start with maintainers, then internal users, then a controlled production percentage.
- Record bridge usage. A migration is not progressing if the new component ships while every consumer still uses the adapter.
- Delete the bridge only after the last dependency is gone. Removal is a milestone, not the starting assumption.
This is also where the advice in Modern CSS in 2026 needs a qualification. Native CSS features can remove build-time and runtime costs, but the architectural benefit does not justify a global cutover by itself. The migration method is part of the architecture.
The browser is changing the contract, not just the syntax
The platform is making more design-system behavior declarative, which increases the value of a clean boundary between component meaning and implementation mechanics.
Interop 2026 identifies anchor positioning, container style queries, dialogs and popovers, scroll-driven animations, view transitions, and contrast-color() among its focus areas. These are not isolated syntax additions. They move responsibilities that teams previously implemented in JavaScript or build tooling into the browser, while making cross-browser behavior measurable through shared tests (Interop 2026).
Chrome’s 2026 Web UI roundup shows the same direction: light-dark(), @function, style queries, if(), and feature detection for at-rules are being presented as composable primitives for themed and adaptive interfaces (Chrome for Developers). A component library that hardcodes every theme decision in JavaScript will have a harder migration path than one that already separates tokens, state, and layout responsibilities.
Consider a theme boundary that can adopt new CSS capabilities without changing the component API:
/* The component API exposes --surface and --content, not browser-specific logic. */
.card {
--card-surface: var(--surface, white);
--card-content: var(--content, black);
color: var(--card-content);
background: var(--card-surface);
}
@supports (color: light-dark(black, white)) {
.card {
color: light-dark(var(--content-light), var(--content-dark));
background: light-dark(var(--surface-light), var(--surface-dark));
}
}
The fallback remains explicit, while the enhancement is localized. The component consumer does not need to know whether the browser, a stylesheet, or a JavaScript theme manager selected the value. That is the payoff of a reversible boundary: internal mechanics can change without turning every consumer into a migration project.
The same principle applies to overlays. The web.dev anchor-positioning guide documents anchor-scope, position-area, fallback positions, and position-visibility: anchors-visible for reusable anchored elements (web.dev anchor positioning). A menu component should expose “open relative to this trigger,” not force every consumer to understand getBoundingClientRect(), scroll listeners, and viewport collision math. A platform migration is safer when the API describes the relationship that users need rather than the workaround the old implementation required.
What to measure before calling a migration successful
A migration is successful when it improves the system’s constraints without transferring cost to consumers. “The old dependency is gone” is evidence of completion, not evidence of value.
Use a scorecard with four layers:
- Correctness: visual diffs, interaction paths, keyboard navigation, accessibility-tree checks, and theme fixtures.
- Compatibility: percentage of consumers on the new path, remaining adapter calls, unsupported overrides, and rollback frequency.
- Performance: server-side render time, component initialization, stylesheet bytes, client scripting, and cache behavior.
- Operating cost: migration throughput, repair time, bridge maintenance, and the number of exceptions added during rollout.
GitHub’s report is useful because it includes more than a final technology choice: it describes the migration sequence, the sx inventory, the number of engineers involved at different stages, and measured performance changes. Its later phase moved from 895 remaining sx props to zero in three weeks with two engineers and Copilot coding agents, after an earlier phase had migrated 6,419 props over six months with a larger rotation. Those figures should not be copied as a forecast; they show why migration phases and automation level need to be tracked separately (GitHub Engineering).
Do not use a single “adoption” number. A component can have 95% import adoption and still be unhealthy if the remaining 5% contain the hardest themes, the most customized consumers, or the highest-traffic pages. Segment by risk: checkout, authentication, editor surfaces, accessibility-sensitive flows, and applications with independent release schedules deserve separate rollout signals.
And do not let AI turn a reversible migration into a faster rewrite. Coding agents are excellent at repetitive translations and codemods, but they do not remove the need to define the contract, select representative fixtures, or inspect exceptions. This is the same evaluation problem discussed in AI-generated interfaces need a design benchmark: generated output needs a benchmark that resembles production work, not a polished demo.
The migration decision: replace, wrap, or wait
Not every old styling system deserves immediate removal. Choose the path based on the constraint that is actually hurting users or maintainers.
Replace directly when the public contract is narrow, consumers are few, and automated fixtures cover the relevant states. A small, stable component can move without a long-lived adapter.
Wrap temporarily when the implementation is the problem but consumer behavior is valuable. Preserve the old entry point, route it to the new implementation, and instrument the wrapper so the exit date is measurable.
Wait and isolate when the system lacks representative tests, has undocumented escape hatches, or depends on a theme model that the new implementation cannot express. Creating a contract inventory and fixtures may be the highest-value migration work for the first quarter.
The wrong question is “Which framework should replace this one?” The better question is “What is the smallest boundary at which we can prove the new behavior, reverse it, and see who still depends on the old behavior?”
That is why the durable migration unit is the component. A component has a public contract, a bounded visual and interaction surface, identifiable consumers, and a plausible rollback path. A rewrite has none of those guarantees. When the browser, runtime, or design language changes again, the teams that can migrate one proven boundary at a time will move faster than the teams still waiting for a perfect cutover weekend.