React 19 forced our hand - MUI v4 was incompatible and had to go. But when I started digging into what MUI was actually doing under the hood, the React 19 issue turned out to be the least interesting part of the problem.
Why Not Just Upgrade to MUI v5?
Upgrading from MUI v4 to v5 would have solved the React 19 incompatibility - but not the underlying problem. MUI v5 swapped makeStyles for @emotion, which is still CSS-in-JS, still injecting styles at runtime. The Shadcn conflicts wouldn't go away either, since the two libraries would still be competing at the style level.
And practically speaking, MUI v4 to v5 is not a small migration. The API changed significantly - component props, theming, styling APIs, all of it. Across a 4-year-old codebase where MUI was used in hundreds of places, that migration would have been weeks of work with high regression risk.
Re-implementing the components in TailwindCSS meant doing that work once, getting rid of CSS-in-JS entirely, and ending the Shadcn conflicts for good.
The Approach: Reading MUI's Source Code
Rather than guessing at the behavior of each component, I cloned the MUI v4 repository and read through the source code directly - component by component, tracing internal hooks, event handling, accessibility attributes, edge cases, and the makeStyles runtime to understand exactly what each component was doing under the hood.
This wasn't optional. The app had been in development for 4 years, and these 25 components had been used across 300+ pages. Every prop, every edge case, every subtle behavior was potentially relied upon somewhere. A replacement that was "mostly the same" would have introduced regressions that were impossible to fully audit.
So each component was rebuilt as a 1:1 equivalent - same public API, same behavior, same edge cases - with makeStyles stripped out and replaced by static Tailwind classes. Then shipped incrementally, component by component, so any regressions could be caught in isolation rather than all at once.
No visual regressions reached production.
Design Tokens: Keeping MUI and Tailwind in Sync
Replacing MUI wasn't just about rebuilding components. We also had to make sure the existing application looked the same while MUI and Tailwind were running side by side.
I created a single source of truth for our design tokens — an object containing the application's colors and other shared values. From that source, I generated the configuration for both MUI and Tailwind, so the two systems couldn't gradually drift apart.
For theming, I used CSS variables such as --color-text-primary and switched their values at runtime. This allowed us to support the application's existing themes — including light, dark, and cosmic — instead of being limited to Tailwind's default light/dark approach.
Running both styling systems side by side also introduced some subtle conflicts. MUI and Tailwind/shadcn could both apply styles to the same elements, causing class name conflicts and unexpected style overrides. Their different z-index scales also caused some overlays to appear behind other components. These issues had to be identified and resolved as components were migrated.
The goal wasn't to redesign the application while migrating it. It was to replace the underlying styling system while minimizing visual changes for users.
Removing the Runtime Styling Layer
One of the biggest changes was replacing makeStyles with static Tailwind utility classes.
MUI's styling system performed work at runtime to generate and manage styles - injecting CSS into the DOM via <style> tags and hooking into React's context to read the current theme. For highly interactive components, that work showed up clearly in the React DevTools Profiler: each render was doing significantly more than it needed to.
Tailwind has no runtime. Utility classes are resolved at build time, the CSS is static, and there's nothing to inject or recalculate. Swap makeStyles for Tailwind classes and the component just renders - nothing else.
Autocomplete was the most noticeable example, rendering roughly 12x faster after the rewrite. Across all 25 components, render times improved by roughly 3x-12x.
I also replaced MUI's runtime theme usage where possible:
- Static design tokens → Tailwind classes
- Dynamic values → CSS variables
- 110
useThemecalls → removed - 50
makeStylesusages → removed
Results
- 3x-12x faster render times across 25 re-implemented components
- 20% bundle size reduction from removing MUI entirely
- Shadcn conflicts eliminated - one styling system, no more specificity battles
- Zero visual regressions in production
This post is part of a series on scaling a frontend platform to 18 apps. Read the full overview.