Migrating from Material UI (MUI) to shadcn/ui is less about “swap a component library” and more about shifting your UI architecture: you move from a theme-driven, component-library model (MUI + Emotion/Styled Components) to a headless + Tailwind + copy-the-source model with first‑class accessibility and full control over markup.
Below is a practical, end‑to‑end migration guide you can follow for real projects.
1. Understand the Architectural Shift
Before touching code, be clear about what’s changing.
MUI model
- Central theme object (palette, typography, spacing).
- Components styled via:
sxpropstyledAPImakeStyles/withStyles(legacy)- global theme overrides.
- Heavy reliance on Emotion or styled-components runtime (unless using the CSS‑only variant).
shadcn/ui model
- Not a “library” in the usual sense; it’s a set of copy‑pasteable components (via CLI) that live in
components/ui. - Styling uses Tailwind CSS utility classes.
- Accessible primitives are based on Radix UI or Base UI, depending on your setup.[1][2]
- You are expected to own and modify the component source.
Implications when migrating from MUI to shadcn/ui:
- You will:
- Replace MUI components with shadcn/ui counterparts or custom components.[4]
- Migrate MUI’s design tokens into Tailwind config.
- Replace Emotion/styled CSS with Tailwind classes (and a bit of custom CSS where needed).[3][4]
- Not all MUI components exist in shadcn/ui:
- DataGrid, DatePicker, and some lab components will need external libs or custom builds.[4]
2. Plan the Migration Strategy
Do not big‑bang replace everything at once. Use an incremental strategy similar to large‑scale migrations described for other libraries.[1][3]
2.1 Goals
Typical goals when moving MUI → shadcn/ui:
- Consistency: standardize on shadcn/ui + Tailwind for all new UI.[3]
- Performance: reduce bundle size by removing Emotion runtime and unused MUI code.[3][4]
- Flexibility: gain full control over markup and styling; easier to match custom design systems.[6]
2.2 Phased approach
Model your migration as a multi‑phase initiative (similar to the Coder team’s MUI→shadcn migration epic).[3]
Phase 1 – Foundation
- Add Tailwind + shadcn/ui.
- Mirror your MUI theme into Tailwind.
- Decide on icon library (Lucide is the default with shadcn).[3]
Phase 2 – Core components
- Migrate Button, TextField/Input, Tooltip, Dialog, basic navigation.[3]
- Introduce Tailwind style patterns where MUI
sx/styledwere heavily used.
Phase 3 – Complex & styling system
- Tables, forms, cards, layout primitives, data display, icons.[3]
- Replace Emotion CSS‑in‑JS with Tailwind classes and minimal CSS modules/
globals.css.[3][4]
Phase 4 – QA and cleanup
- Regression tests, accessibility checks, remove MUI and Emotion dependencies.[3][4]
2.3 Working branch and safety
Create a dedicated migration branch, as recommended for similar migrations.[2]
git checkout -b mui-to-shadcn-migration
Use small PRs per feature/route/component group for easier review and rollback.[3]
3. Set Up Tailwind and shadcn/ui
If your project is not yet using Tailwind, do that first; shadcn/ui is built on top of Tailwind.
3.1 Install Tailwind CSS
Follow the official Tailwind install steps for your stack (Next.js, Vite, etc.). In general:
- Install
tailwindcss,postcss,autoprefixer. - Create
tailwind.configandpostcss.config. - Add Tailwind directives to your global CSS:
@tailwind base;
@tailwind components;
@tailwind utilities;
Ensure your content paths include your React files and the components directory.
3.2 Initialize shadcn/ui
Use the shadcn CLI to bootstrap the system (the CLI is similar to how you’d configure Base UI vs Radix UI in components.json).[1][2][7]
- Run the project creator CLI command generated from the docs or UI generator.[2]
- This:
- Creates (or updates)
components.json. - Sets up
components/uiand utility files (e.g.,lib/utils.tsforcn()). - Optionally chooses style presets.
From here, you can add components like:
pnpm dlx shadcn-ui@latest add button input dialog
Each component is copied into your repo, so you own it.
4. Mirror the MUI Theme into Tailwind
Your goal here is to preserve the look & feel of your app so users don’t notice a visual regression.
4.1 Inventory your MUI theme
Find where your MUI theme is defined, typically:
const theme = createTheme({
palette: { primary: {...}, secondary: {...}, ... },
typography: { fontFamily, h1: {...}, body1: {...}, ... },
spacing: 8,
shape: { borderRadius: 8 },
components: { ...overrides }
});
Document:
- Color tokens: primary, secondary, error, warning, success, background, text.
- Typography scale (font families, weights, sizes).
- Border radius, spacing scale.
- Common component overrides (Button: variant sizes, Input: border styles, etc.).
4.2 Translate to Tailwind config
In tailwind.config, extend theme with equivalents:
theme.colors→ your palette.theme.fontFamily→ typographic families.theme.borderRadius→ MUI’sshape.borderRadius.theme.spacingor just rely on Tailwind’s default and adjust where necessary.
This makes it easy to replicate your MUI visual language with Tailwind utility classes.[4]
5. Decide on Coexistence Strategy
MUI and shadcn/ui can run side‑by‑side during migration:
- Keep MUI ThemeProvider around while some components still use MUI.
- Use Tailwind classes for new/shadcn components.
- Avoid wrapping the entire app in conflicting global resets; Tailwind’s preflight and MUI’s baseline can coexist, but test typography and margins carefully.
You will be in a mixed state for some time; that is expected and manageable if you:
- Migrate route by route or feature by feature.
- Avoid editing old MUI components, except to slowly wrap them with temporary adapters if needed.
6. Map MUI Components to shadcn/ui
You need a mapping plan: what replaces what.
Below is a typical mapping (exact component names may differ, but conceptually):
| MUI | shadcn/ui / Tailwind | Notes |
|---|---|---|
Button |
Button |
Very straightforward migration. |
TextField / Input |
Input, Textarea, Label, Form helpers |
Use separate components instead of one giant TextField. |
Dialog / Modal |
Dialog |
Under the hood uses Radix/Base primitives.[1][2] |
Menu / Popover |
DropdownMenu, Popover |
Similar behavior; different API. |
Tabs |
Tabs |
Similar, but you own markup. |
Tooltip |
Tooltip |
Based on Radix/Base primitives. |
Snackbar / Alert |
Toast or custom Alert |
You may implement your own stack. |
AppBar / Toolbar |
Custom layout components + Tailwind | Create layout primitives in components/ui. |
Card |
Card |
Tailwind classes for variants. |
DataGrid, DatePicker |
External libs or custom | No direct shadcn equivalents; plan extra work here.[4] |
For missing MUI components (DataGrid, DatePicker, complex pickers): choose dedicated libraries and style them with Tailwind.[4]
7. Migrate Core Components Step‑by‑Step
Use the same layered approach recommended in other shadcn migrations: start with independent components, then composite ones, then business components.[1][3]
7.1 Buttons
- Install the shadcn
Buttoncomponent via CLI. - Compare your MUI Button variants to shadcn’s:
variant="contained"≈variant="default"(with background).variant="outlined"≈variant="outline".variant="text"≈variant="ghost"orlink.
- Optionally write a temporary adapter component that maps old props to new ones so you can change imports without touching every call site at once.
Example adapter (simplified):
import { Button as ShadcnButton } from "@/components/ui/button";
type Props = {
variant?: "contained" | "outlined" | "text";
} & React.ComponentProps<typeof ShadcnButton>;
export function AppButton({ variant = "contained", ...props }: Props) {
const mappedVariant =
variant === "contained"
? "default"
: variant === "outlined"
? "outline"
: "ghost";
return <ShadcnButton variant={mappedVariant} {...props} />;
}
Then:
- Replace
import { Button } from "@mui/material";→import { AppButton as Button } from "@/components/app-button";in a gradual, feature‑by‑feature way.
7.2 Inputs and TextFields
MUI’s TextField is very “kitchen sink”; shadcn/ui tends to split responsibilities:
LabelInputorTextareaFormMessage/FormDescription- Possibly
Formwrappers with Zod/React Hook Form integration.
Migration approach:
- Start with simple cases (plain text inputs with minimal adornments).
- For more complex
TextFields (adornments, validation messages, select‑like behavior), build reusable patterns incomponents/ui, leveraging shadcn’s form examples.
7.3 Dialogs, Menus, Popovers
shadcn’s components wrap Radix/Base UI primitives, similar to how migration is described for Radix → Base UI.[1][2]
Differences vs MUI:
- Often more declarative and explicit:
DialogTrigger,DialogContent,DialogHeader,DialogFooter, etc.- You are expected to own the mark‑up and transitions.
Strategy:
- Replace simple dialogs first (no dynamic forms, minimal state).
- Carefully port more complex modals with nested forms and focus management, verifying keyboard navigation and ARIA attributes as part of testing.[2]
8. Replace Emotion/MUI Styling with Tailwind
One of the biggest tasks is converting your styling system, as noted in real‑world MUI→shadcn migrations.[3][4]
8.1 Inventory styling patterns
Search for:
sx={...}styled(usage.makeStyles/withStyles.- Global component overrides in
theme.components.
For each pattern, pick a Tailwind strategy:
- Simple layout / spacing / display → pure Tailwind classes in JSX.
- Reusable component styles → encapsulated in shadcn/ui components or your own wrappers (use
cva()for variants). - Global typography / base styles → configure via Tailwind and global CSS.
8.2 Migrate gradually
Do not attempt to convert all styling in one go.[1][3]
- When you migrate a component from MUI to shadcn:
- Replace its styles with Tailwind.
- Move any special visual variants into a
classNameor variant system usingcva.
Example of a “MUI variant” replaced by cva:
import { cva } from "class-variance-authority";
export const badgeVariants = cva(
"inline-flex items-center rounded-full px-2 py-1 text-xs font-medium",
{
variants: {
color: {
primary: "bg-primary text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground",
},
},
defaultVariants: { color: "primary" },
}
);
9. Handle Components Without Direct Equivalents
As noted in comparisons, DataGrid and DatePicker have no first‑class equivalents in shadcn/ui yet.[4] You have three options:
- Keep MUI just for those components temporarily.
- Replace with a specialized library (e.g., AG Grid, TanStack Table + your own styling,
react-day-pickerfor date selection) and wrap them in your own Tailwind‑styled components. - Implement simplified custom versions using Radix/Base primitives and Tailwind (more work, more control).
Plan extra time; a realistic migration estimate is 3–8 weeks depending on component count and complexity.[4]
10. Icons Migration
If you currently use @mui/icons-material:
- Decide on a new icon set; Lucide is common with shadcn/ui.[3]
- Create an
Iconcomponent that wraps your icon library with consistent size, color, and className props. - Gradually replace
import XIcon from "@mui/icons-material/X";with new imports.
This is usually a fast but repetitive step; schedule a specific phase for it as the Coder migration plan does.[3]
11. Accessibility & Behavior Testing
Because shadcn/ui is built on accessible primitives (Radix/Base UI), you get good defaults, but your customizations need verification.[1][2]
Follow a test checklist similar to the Base UI migration guide.[2]:
- All migrated components render correctly.
- All interactive components:
- Open/close via mouse and keyboard.[2]
- Manage focus correctly (dialog open/close, menus, etc.).[2]
- Keyboard navigation works (Tab/Shift+Tab, arrow keys where expected).[2]
- Screen reader announcements and ARIA roles are appropriate.[2]
- Animations and transitions don’t break interactions.[2]
- Layout is responsive and dark mode (if you use it) still functions.[2]
Run your automated tests (unit, integration, e2e) as part of each migration PR.
12. Clean Up Dependencies
Once all targeted components are migrated:
- Identify MUI usage:
- Search for
@mui/material,@mui/icons-material,@mui/lab. - Ensure no residual imports remain.
- Uninstall MUI and Emotion:
- Remove
@mui/*packages and Emotion (@emotion/react,@emotion/styled) frompackage.json. - Similar to how Radix UI packages are removed when migrating to Base UI.[1][2]
- Run a final pass of unused code/dependency removal.
13. Documentation and Team Adoption
As with other shadcn migrations, documentation is a key final step.[2][3]
- Update your internal UI guidelines:
- How to create new components (copy from shadcn docs, then customize).
- How to use Tailwind and
cvafor variants. - Replace references to MUI in READMEs and wiki with shadcn/ui + Tailwind instructions.[2]
- Provide example patterns for common UI:
- Forms, dialogs, lists, tables, alerts, navigation.
This helps prevent new code from reintroducing MUI style patterns.
14. Rough Timeline and Effort
Based on comparisons and real migrations:[3][4][6]
- Small app (dozens of components): 2–3 weeks.
- Medium app (hundreds of components): 4–8 weeks.
- Large enterprise app: several sprints, depending on team size and test coverage.
Complex areas like DataGrid, DatePicker, and legacy styling will dominate effort.[4]
15. Practical Migration Checklist
You can use the following as a working checklist:
- [ ] Create migration branch and plan phases.[2][3]
- [ ] Install Tailwind + configure theme to match MUI.[4]
- [ ] Initialize shadcn/ui and set up core utilities.[7]
- [ ] Migrate Button (with adapter if needed).
- [ ] Migrate typography components and layouts.
- [ ] Migrate Input/TextField patterns.
- [ ] Migrate Dialog, Menu, Tooltip, Popover.
- [ ] Migrate Card, List, basic data display.
- [ ] Choose and integrate replacements for DataGrid and DatePicker.[4]
- [ ] Replace Emotion/MUI styling patterns with Tailwind per component.[3][4]
- [ ] Migrate icons (MUI icons → Lucide or equivalent).[3]
- [ ] Run accessibility and regression tests for each feature area.[2][3]
- [ ] Remove MUI and Emotion dependencies.
- [ ] Update documentation and team guidelines.[2][3]
If you want, you can share a small MUI component (e.g., a form or dialog), and I can show the before/after code migrated to shadcn/ui + Tailwind to make this guide concrete.
No comments:
Post a Comment