The migration playbook
How this moves into the real UpNext app, phase by phase. Anti-patterns to avoid.
This is the plan for moving the design system from the Kitchen counter playground into the real UpNext app. It's the eventual point of all the architecture decisions in the other docs.
The goal of the migration
Not "replace every component overnight." That's how migrations fail. The goal is:
Every new feature uses UpNext wrappers from day one, and existing screens migrate page-by-page on a deliberate schedule.
The end state is: the old ad-hoc components are gone, every UI element in UpNext is an <UpNext /> wrapper, and the design system in the playground is the literal source of truth for production.
The phases
Phase 0 — Sign-off on the playground (we're here)
Before any code moves into the UpNext repo, the playground has to be done done. Specifically:
- All token decisions locked. Brand color final. Neutral ramp final. Status colors final. No "we'll come back to this."
- All wrappers built, every state polished, every variant validated.
- Living docs written and reviewed.
- One full design review with the team — designers and engineers in the room — looking at every component, agreeing this is the standard.
The reason: once components migrate into UpNext, they get consumed. Changing them later requires updating consumers. The playground exists exactly so we can iterate freely before incurring that cost.
Phase 1 — Set up the system in the UpNext repo
Don't copy files manually. Publish the wrappers as a workspace package inside the UpNext monorepo (e.g. packages/upnext-ui). Then:
globals.csslives in the package, app imports it viaimport "@upnext/ui/globals.css"in the root layoutcomponents/upnext/*becomespackages/upnext-ui/src/components/*- The ESLint guardrail moves into the package and the repo's root config extends it
lib/tokens.tsships in the package for design tooling (Figma plugin, etc.)
This means: future changes to the design system happen by editing the package, not by hunting through app code. The migration playbook itself uses the migration playbook.
Engineer note: publishing locally inside the monorepo (workspace package) is much easier than publishing to npm — same import ergonomics, no versioning ceremony, instant updates.
Phase 2 — Adopt for net-new screens first
Every new feature built after Phase 1 uses @upnext/ui from day one. No exceptions. This is the easy part — there's no migration cost for code that doesn't exist yet.
What this gets us:
- Real-world feedback on the system from people building real features
- Inventory of any wrappers that are missing (every "I needed a thing that wasn't there" is a wrapper to add)
- New code stops contributing to the inconsistency problem
Phase 3 — Migrate existing screens, page by page
Pick a page. Migrate every component on it to the wrapper equivalent. PR the change. Move to the next page.
Migrate by route, not by component type. Don't try to "migrate all buttons across the app in one PR." That's enormous, hard to review, and likely to break things in screens you don't think about. Instead, fully migrate one route at a time — every component on that page goes through, you ship it, you verify it, you move on.
Suggested order:
- Settings screens first — usually contained, low traffic, good for shaking out migration friction.
- Then most-trafficked authenticated screens — the value of consistency compounds with use.
- Save marketing / landing pages for last — they often have one-off treatments that are intentional, and they get less daily eyes on them.
A migration PR for one route typically looks like:
- Replace every
importfrom app-local components to@upnext/ui - Remove any local color overrides (
className="bg-[#1464FF]"etc.) — the wrapper handles brand color now - Delete the now-unused local component files
- Visual diff the route against the previous version — confirm nothing regressed
Phase 4 — Retire the old components
As local components get fully unused (no remaining imports), delete them. The codebase actively shrinks as the migration progresses — that's the signal we're doing it right.
Also: turn the ESLint rule that blocks deep @/components/ui/* imports into an error (it might start as a warning). The guardrail is now enforced everywhere.
Phase 5 — Sunset the playground (or don't)
After full migration, the playground can either:
- Stay as the canonical reference and demo environment for future component work. Recommended. It's where the next iteration happens.
- Or be archived if the team decides it's added enough value. Not recommended — the next time someone needs to add a component, they'll wish it still existed.
Migration anti-patterns to watch for
- "Let's migrate Button across the whole app in one PR." Don't. Migrate by route.
- "We'll override the wrapper for this one screen." Don't. If a screen needs something different, that's a new wrapper (or a new variant on an existing one) — add it to the system, then the screen uses the system.
- "We can skip the ESLint rule for now." Don't. The rule is what prevents the system from collapsing back into ad-hoc-land. If it's flagging something, that's exactly what it's supposed to do.
- "The new wrapper looks slightly different from the old component, let's tweak it to match." Don't. The old component was the wrong thing — that's why we're migrating. The wrapper looks how the design system says it should look. The migration is the moment to align, not to perpetuate divergence.
Migration FAQ
Q: What if we discover the wrapper is wrong during migration? A: Update the wrapper in the playground, ship it, update the package version (or re-publish workspace-locally), then continue the migration. The playground is still the source of truth during the migration — that's the whole point.
Q: What if we need a feature flag during migration? A: Wrap the wrapper at the app level with the feature flag, not inside the design system. The DS doesn't know about feature flags; that's app concern.
Q: Can we migrate just colors first (use the new tokens) without migrating to wrappers?
A: You can, but you won't get most of the benefit. The wrappers are what guarantee consistency — tokens alone still let someone use --primary on a custom button that ignores all the other system rules. Wrap and tokenize together.
Q: How long will the full migration take? A: Depends on app size and team capacity. As a rough heuristic: a focused engineer can migrate ~1 route per day for medium-complexity routes. Plan accordingly. The migration doesn't need to block other work — it happens in parallel.