Introduction: Why We Replaced MUI with shadcn/ui
Migrating a production React application from Material UI (MUI) to shadcn/ui involves replacing a monolithic, runtime-styled component library with a composable, Tailwind CSS-based component architecture that gives you full ownership of every component's source code. The result is dramatically smaller bundles, faster load times, a consistent design system, and a codebase that AI coding assistants like Claude, Cursor, and GitHub Copilot can understand and extend reliably.
Written in September 2026, this case study documents how MetaDesign Solutions executed this migration for a client's enterprise SaaS dashboard — a React application with 120+ screens, 40+ shared components, and a growing frustration with MUI's performance overhead and styling inconsistencies. The client's development team had adopted AI-augmented development workflows → but found that AI tools consistently generated inconsistent code because the existing codebase lacked a clearly defined, discoverable design system.
This post covers the full journey: the audit that exposed the problems, the architecture decisions behind the new component library, the migration strategy that avoided a risky big-bang rewrite, and the rules.md file we created to make the codebase genuinely AI-native.
The Problem: MUI at Scale Creates Hidden Technical Debt
Material UI is an excellent library for rapid prototyping and getting to market fast. But as applications scale beyond 50+ screens, several problems emerge that compound over time:
1. Bundle Size Bloat
Our audit of the client's application revealed alarming numbers:
| Metric | Before (MUI) | Industry Target | Severity |
|---|---|---|---|
| Total JS bundle (gzipped) | 1.8 MB | < 300 KB | Critical |
| MUI + Emotion CSS-in-JS | 420 KB | N/A | Major contributor |
| First Contentful Paint | 4.2s | < 1.8s | Critical |
| Largest Contentful Paint | 6.1s | < 2.5s | Critical |
| Time to Interactive | 8.7s | < 3.8s | Critical |
2. No Coherent Design System
Over two years of feature development, the codebase had accumulated six different ways to style components: MUI's sx prop, styled() API, inline style attributes, external CSS files, CSS modules, and makeStyles from the deprecated MUI v4 API that was never cleaned up. New developers spent their first week just figuring out which styling approach to use.
3. Unnecessary Library Sprawl
The dependency audit found 14 UI-related packages that overlapped with MUI's own capabilities — separate date pickers, icon libraries, modal packages, toast libraries, and chart wrappers. Many were installed once for a single feature and never consolidated.
4. AI Tools Produced Inconsistent Code
When developers used Claude, Cursor, or Copilot to generate new components, the AI had no way to determine the project's "correct" styling approach. It would generate MUI components with sx props in one file, styled() in another, and raw CSS in a third — directly mirroring the inconsistency already in the codebase. There was no single source of truth for the design system.
Phase 1: Full Codebase Audit and Component Inventory
Before writing a single line of migration code, we invested two weeks in a systematic audit. This phase is critical — skipping it leads to scope creep, missed components, and regressions.
Component Census
We catalogued every MUI component used across the application using static analysis:
npx depcheck --json | jq '.dependencies' # Unused packages
grep -rn "from '@mui" src/ | awk -F'/' '{print $NF}' | sort | uniq -c | sort -rn
grep -rn "from 'react-" src/ | awk -F"'" '{print $2}' | sort | uniq -c | sort -rn
The results showed:
- 38 unique MUI components used across the app (Button, TextField, Dialog, DataGrid, Autocomplete, etc.)
- 14 external UI packages providing overlapping functionality
- 23 components used only once — candidates for inlining or removal
- 9 custom wrapper components that simply proxied MUI with one or two overridden props
Styling Pattern Audit
We mapped every styling approach to its usage count:
| Styling Method | File Count | Decision |
|---|---|---|
MUI sx prop | 187 | Replace with Tailwind classes |
MUI styled() API | 64 | Replace with Tailwind + cn() |
Inline style attributes | 92 | Replace with Tailwind classes |
| CSS Modules | 31 | Migrate to Tailwind |
Deprecated makeStyles | 43 | Remove entirely |
| External CSS files | 28 | Consolidate into Tailwind |
Performance Profiling
Using Lighthouse, WebPageTest, and Chrome DevTools Performance panel, we identified the primary bottlenecks:
- Emotion CSS-in-JS runtime: Generating styles at runtime added ~200ms to every page transition
- MUI ThemeProvider: Context re-renders propagated through the entire component tree on any theme token access
- Unused MUI modules: Tree-shaking was partially effective, but Emotion's dynamic styles resisted static elimination
- Duplicate icon bundles: Both
@mui/icons-material(2,100+ icons) andreact-iconswere installed — only 47 unique icons were actually used
Why shadcn/ui: The Architecture Decision
We evaluated four alternatives to MUI before recommending shadcn/ui to the client:
| Library | Bundle Impact | Customization | AI Compatibility | Verdict |
|---|---|---|---|---|
| Ant Design | Similar to MUI | Theme tokens | Moderate | Rejected — same class of problems |
| Chakra UI | Lighter, still runtime | Style props | Moderate | Rejected — runtime CSS |
| Radix Primitives (raw) | Minimal | Full control | Good | Too much boilerplate |
| shadcn/ui | Zero runtime | Full source ownership | Excellent | Selected |
shadcn/ui won decisively for three architectural reasons:
- Copy-paste ownership: Components are copied into your project as source files in
src/components/ui/. There is nonode_modulesdependency — you own the code. This means you can modify, extend, and delete components without worrying about upstream breaking changes. AI tools can read the actual component source, not guess at library internals. - Tailwind CSS-only styling: No runtime CSS-in-JS. Every style is a Tailwind utility class compiled at build time into a single, minimal CSS file. This eliminates Emotion's runtime overhead entirely — no style injection, no context providers, no re-renders from theme changes.
- Radix Primitives foundation: shadcn/ui builds on Radix UI primitives for accessibility-critical components (Dialog, Dropdown, Tooltip, Tabs). This gives you WAI-ARIA compliance without reinventing focus management, keyboard navigation, or screen reader support.
Phase 2: Designing the Component Library
Before migrating a single component, we designed the target architecture. This upfront investment saved weeks of rework and ensured every team member — human and AI — would build consistently.
Directory Structure
src/
components/
ui/ # shadcn/ui base components (Button, Input, Dialog, etc.)
composed/ # Multi-component compositions (SearchBar, DataTable, StatCard)
layouts/ # Page-level layouts (DashboardLayout, AuthLayout)
providers/ # Context providers (ThemeProvider, ToastProvider)
lib/
utils.ts # cn() helper, formatters, validators
styles/
globals.css # Tailwind directives + CSS custom properties
tokens.css # Design tokens (colors, spacing, typography)
hooks/ # Shared React hooks
Design Token Architecture
We defined a design token system using CSS custom properties that Tailwind consumes. This creates a single source of truth for the entire visual language:
/* tokens.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--destructive: 0 84.2% 60.2%;
--border: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode overrides */
}
Component API Standards
Every component in the library follows strict API conventions, documented for both human developers and AI assistants:
- Variants via CVA: All visual variants use
class-variance-authority(CVA) —variant,size,colorprops map to predefined Tailwind class sets - Composition via
cn(): Thecn()utility (built onclsx+tailwind-merge) is the only way to merge classes — no string concatenation, no conditional ternaries inclassName - Forwarded refs: Every component uses
React.forwardReffor DOM access compatibility - Typed props: Every component exports its props type —
ButtonProps,InputProps, etc.
Phase 3: Incremental Migration Strategy — No Big Bang
We rejected a big-bang rewrite in favor of an incremental, page-by-page migration that kept the application deployable throughout the entire process. Here is the strategy we followed:
Step 1: Install shadcn/ui Alongside MUI (Week 1)
Both libraries coexist during migration. Tailwind CSS was added and configured to ignore MUI-rendered elements using Tailwind's important strategy to prevent class conflicts:
// tailwind.config.ts
export default {
// Prevent Tailwind from affecting MUI components during migration
important: '#app-root',
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
// ... map all design tokens
},
},
},
} satisfies Config;
Step 2: Migrate Leaf Components First (Weeks 2-3)
We started with the simplest, most-used components — Button, Badge, Input, Card — that had no child component dependencies. Each migration followed a checklist:
- Install the shadcn/ui component:
npx shadcn@latest add button - Create a compatibility wrapper that accepts the old MUI props API
- Run the component through Storybook visual regression tests
- Search-and-replace all imports from
@mui/material/Buttonto@/components/ui/button - Remove the compatibility wrapper once all consumers use the new API
- Delete the old MUI imports and verify no references remain
Step 3: Migrate Complex Components (Weeks 4-6)
DataGrid, Autocomplete, DatePicker, and Dialog required more careful handling. For DataGrid, we built a custom DataTable component using @tanstack/react-table + shadcn/ui's Table primitives. The result was a component that was 4x smaller and gave us full control over column rendering, sorting, filtering, and pagination.
Step 4: Remove MUI Entirely (Week 7)
npm uninstall @mui/material @mui/icons-material @mui/x-data-grid \
@mui/x-date-pickers @emotion/react @emotion/styled \
@mui/lab @mui/styles
After the final MUI removal, we ran a full build and verified zero references to @mui remained in the codebase.
Phase 4: Eliminating Unnecessary Dependencies
With MUI gone, we turned to the 14 overlapping UI packages that had accumulated over two years. Each was evaluated against a simple criteria: does shadcn/ui or Tailwind already provide this capability?
| Removed Package | Size | Replaced With |
|---|---|---|
| react-modal | 32 KB | shadcn/ui Dialog (Radix) |
| react-toastify | 18 KB | shadcn/ui Toast (Sonner) |
| react-select | 45 KB | shadcn/ui Combobox (cmdk) |
| react-datepicker | 52 KB | shadcn/ui DatePicker (react-day-picker) |
| react-tooltip | 14 KB | shadcn/ui Tooltip (Radix) |
| @mui/icons-material | ~60 KB (used) | lucide-react (47 icons cherry-picked) |
| react-icons | ~80 KB (used) | lucide-react (consolidated) |
| notistack | 24 KB | Removed (duplicate of react-toastify) |
In total, we removed 18 npm packages and replaced them with 3 — shadcn/ui's Radix primitives, @tanstack/react-table, and lucide-react.
Expert Solutions for Web Development
Need help with Web Development? Our engineering team builds production-ready solutions tailored to your enterprise workflows.
Phase 5: Making the Codebase AI-Native with rules.md
This was the most forward-thinking phase of the project, and the one the client was most excited about. The goal was to make the codebase self-documenting for AI coding assistants so that every AI-generated component would automatically follow the design system — without developers needing to manually review and correct AI output.
What is rules.md?
A rules.md (or .cursorrules, or CLAUDE.md) file is a project-level instruction file that AI coding platforms read before generating code. It acts as the AI's "onboarding document" — telling it the project's conventions, preferred libraries, component patterns, and anti-patterns to avoid. We created a comprehensive rules.md that Claude, Cursor, and GitHub Copilot Workspace would automatically pick up.
Structure of Our rules.md
# Project Rules for AI Coding Assistants
## Tech Stack
- React 18 + TypeScript (strict mode)
- Tailwind CSS v3.4 for all styling
- shadcn/ui components in src/components/ui/
- Radix UI primitives for accessibility
- @tanstack/react-table for data tables
- lucide-react for icons
- react-hook-form + zod for forms
## Component Creation Rules
1. ALWAYS use components from src/components/ui/ — NEVER install MUI, Chakra, or Ant Design
2. Style ONLY with Tailwind utility classes — NO inline styles, NO CSS modules, NO styled()
3. Use cn() from src/lib/utils for conditional classes — NO string concatenation
4. Every component MUST use React.forwardRef and export its Props type
5. Use CVA (class-variance-authority) for component variants
## Design Tokens
- Colors: Use semantic tokens (bg-primary, text-muted-foreground) — NOT arbitrary values
- Spacing: Use Tailwind scale (p-4, gap-6) — NOT arbitrary px values
- Border radius: Use rounded-md (maps to var(--radius)) — NOT rounded-lg or rounded-xl
## File Conventions
- UI primitives: src/components/ui/[component].tsx
- Composed components: src/components/composed/[component].tsx
- Pages: src/app/[route]/page.tsx
- Hooks: src/hooks/use-[name].ts
## Anti-Patterns (NEVER DO)
- Do NOT use @mui/* or @emotion/* packages
- Do NOT use makeStyles, styled(), or sx prop
- Do NOT use react-modal, react-toastify, react-select (use shadcn equivalents)
- Do NOT create new CSS files — all styles go in Tailwind classes
- Do NOT use arbitrary Tailwind values like bg-[#ff0000] — use design tokens
Why This Matters for AI-Assisted Development
With the rules.md in place, every AI-generated component across the team automatically:
- Uses shadcn/ui primitives from the local
src/components/ui/directory - Styles exclusively with Tailwind utility classes and design tokens
- Follows the project's naming conventions and file structure
- Avoids importing any of the removed libraries
- Uses
cn()for class merging,CVAfor variants, andzodfor validation
The client reported that AI code review rejection rate dropped from 45% to under 8% after the rules.md was introduced — developers spent dramatically less time fixing AI-generated code to match project conventions.
Results: Before and After
After the full migration, the numbers spoke for themselves:
| Metric | Before (MUI) | After (shadcn/ui) | Improvement |
|---|---|---|---|
| JS bundle (gzipped) | 1.8 MB | 680 KB | 62% reduction |
| First Contentful Paint | 4.2s | 1.4s | 67% faster |
| Largest Contentful Paint | 6.1s | 2.1s | 66% faster |
| Time to Interactive | 8.7s | 3.2s | 63% faster |
| npm dependencies | 187 | 134 | 53 packages removed |
| Styling approaches | 6 different methods | 1 (Tailwind only) | Unified system |
| AI code rejection rate | 45% | 8% | 82% improvement |
| Build time | 94s | 41s | 56% faster |
Beyond the numbers, the qualitative improvements were equally significant:
- Developer onboarding: New developers went from "which styling approach do I use?" to productive in a single day
- Design consistency: Every page now uses the same design tokens, spacing scale, and component variants
- Dark mode: Switching themes went from a multi-sprint project to a single CSS variable toggle
- Accessibility: Radix primitives brought WAI-ARIA compliance to Dialog, Dropdown, Tooltip, and Tabs — areas where the previous custom implementations had keyboard navigation gaps
Lessons Learned: What We Would Do Differently
Every migration teaches lessons. Here are the five most impactful takeaways from this project:
- Migrate DataGrid last, not first. Complex table components have the most dependencies and edge cases. We made the mistake of starting DataGrid migration in week 3 and it blocked other work. In future migrations, we would tackle DataGrid in the final phase when the rest of the component library is stable.
- Create the rules.md on day one. We created the rules file in Phase 5, but in hindsight, it should have been the very first deliverable. Developers were already using AI tools during the migration itself, and without rules, the AI kept generating MUI-style code that then needed re-migration.
- Visual regression testing is non-negotiable. We used Chromatic (Storybook-based visual diffing) to catch subtle styling differences between MUI and shadcn/ui implementations. Without it, several components had spacing and alignment regressions that would have reached production.
- Keep MUI as a devDependency until final removal. During the coexistence phase, TypeScript's unused import warnings helped us track migration progress. We could run
grep -rn "@mui" src/ | wc -ldaily to see the count approach zero. - Document the component API mapping before migrating. Creating a spreadsheet mapping every MUI prop to its shadcn/ui equivalent (e.g., MUI's
variant="contained"→ shadcn'svariant="default") saved significant time during the search-and-replace phase.
Should You Migrate from MUI to shadcn/ui?
This migration is not for every project. Here is our honest assessment of when it makes sense:
Migrate If:
- Your bundle size exceeds 1 MB gzipped and Core Web Vitals are failing
- Your team has adopted AI coding tools and wants consistent AI-generated output
- You have 3+ different styling approaches in the same codebase
- You need full design system ownership without library version lock-in
- You are building a modern tech stack and want build-time CSS over runtime CSS
Stay on MUI If:
- Your application is small (< 20 screens) and performance is acceptable
- Your team prefers Material Design's opinionated visual language and does not need customization
- You are heavily invested in MUI's DataGrid Pro/Premium for complex enterprise tables
- Migration cost does not justify the performance gains for your specific use case
Ready to Modernize Your React Frontend?
The MetaDesign Solutions Frontend Architecture team specializes in modernizing legacy React applications — from component library migrations and design system creation to performance optimization and AI-readiness audits. Whether you are struggling with MUI performance, inconsistent styling, or AI tools that generate off-brand code, our engineers can design and execute a migration plan tailored to your application's scale and complexity.
Explore our App Modernization Services →
Or Contact Us → to schedule a free architecture assessment of your current React frontend.



