Migrate Design Systems Gracefully with Multiple Tailwind CSS Configs in Next.js
Yacine Ouardi

Migrating a design system in a live product is one of the riskiest things a frontend developer can face — especially when everything depends on a tightly-coupled, single Tailwind CSS config.
Most teams either:
- Delay migration indefinitely
- Rewrite the whole app (and regret it)
- Or worse, mix styles and create a maintenance nightmare
But when I faced this challenge on a real-world project, I discovered a smarter way: use multiple Tailwind CSS configurations within a single Next.js app — and scope them cleanly using layouts.
This approach lets you run different design systems side-by-side, enabling gradual migration with zero utility class conflicts and no unnecessary duplication.
Why Use Multiple Tailwind Configs?
Tailwind is powerful, but opinionated. It expects a single global config. This is fine for small apps — but when you're dealing with:
- A large-scale product
- Multiple themes
- A legacy design system and a new one in parallel
… then one config becomes a bottleneck.
With multiple configs, you can:
- ✅ Migrate designs gradually, one route or page at a time
- ✅ Avoid class name collisions
- ✅ Keep legacy styles untouched while introducing the new ones
- ✅ Support multiple themes or brands
- ✅ Maintain a clean and scalable codebase
The Key Concepts
There are two main ingredients in this strategy:
- Tailwind’s
@config
directive — to load different Tailwind config files. - Next.js layouts — to scope styles to specific parts of the app.
Together, they allow you to inject design systems per layout — isolated and conflict-free.
Folder Structure
Here’s the base project structure I used:
JavaScript
src/
└── app/
├── (new-design)/
│ ├── designs/new/page.tsx
│ ├── layout.tsx
│ └── new.css
├── (old-design)/
│ ├── designs/old/page.tsx
│ ├── layout.tsx
│ └── old.css
tailwind.old.config.ts
tailwind.new.config.ts
Each design system has:
- Its own folder
- Its own layout
- Its own scoped CSS file
- Its own Tailwind config
Scoped Tailwind Files
Here’s how I scoped each design system.
old.css
css
@config '../../../../tailwind.old.config.ts';
@tailwind base;
@tailwind components;
@tailwind utilities;
new.css
css
@config '../../../../tailwind.new.config.ts';
.new {
@tailwind base;
@tailwind components;
@tailwind utilities;
}
🔒 The .new
class scopes everything inside to use the new design tokens, without polluting global styles.
Layout Setup
Each layout loads the right stylesheet and wraps its children:
(old-design)/layout.tsx
tsx
import './old.css';
export default function OldLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
(new-design)/layout.tsx
tsx
import './new.css';
export default function NewLayout({ children }: { children: React.ReactNode }) {
return <div className="new">{children}</div>;
}
🎯 Now anything rendered inside(new-design)
automatically usestailwind.new.config.ts
.
Real-World Use Cases
This setup is not just for design experiments. It works in serious, production-level apps. Here's where it shines:
- Migrating a legacy UI to a modern design system without downtime
- Supporting white-label themes (each with its own Tailwind config)
- Isolating experiments like new design tokens, spacing scales, or typography
- Managing multi-brand platforms or admin/public dashboards
Example: Two Pages, Two Systems
The following pages share the same markup logic, but different styles:
/designs/old
→ uses the legacy system/designs/new
→ uses the new system
This makes testing and A/B comparisons dead simple — with no conflicts.
Clean Architecture Tips
If you're using nested routes (like in App Router), keep your designs scoped at the folder level, e.g.:
JavaScript
(old-design)/profile/settings
(new-design)/profile/posts
This keeps layouts and styles modular, predictable, and easy to reason about.
Try It Yourself
You can clone the working example here:
👉 taiwlind-mutliconfig GitHub Repo
To run locally:
batchfile
git clone https://github.com/MelancholYA/taiwlind-mutliconfig.git
cd taiwlind-mutliconfig
yarn install
yarn dev
Final Thoughts
This method saved me from a huge rewrite — and made our migration smoother than expected. No hacks. No global overrides. No regrets.
By combining Tailwind’s @config
with scoped layouts in Next.js, you get a flexible, scalable architecture for any design system challenge.
If you're stuck between “rewrite everything” or “keep patching legacy code,” try this middle path — it’s clean, fast, and works with the tools you already use.