The afternoon you lose to a theme override
Anyone who's tried to seriously customise MUI knows the afternoon I'm talking about. You've got a component that's almost right. The design calls for the focus ring to sit a little differently, or the disabled state to use a colour the theme doesn't expose, and you start out confident, because surely there's a prop for this. There's a prop for everything else.
So you reach for the sx prop. Then the theme overrides. Then you're three levels deep in MuiButton.styleOverrides, reading the source to find the class name of an inner element nobody designed you to reach, and eventually you give up and write !important on a selector you're not proud of. The component was built to be configured, not changed, and you've just spent an afternoon discovering the line between the two. That afternoon is what eventually pushed me off MUI for good.
What frameworks genuinely get right
I want to be fair to the framework model first, because the trade-off is real and it's easy to be glib about it. A library like MUI lets you drop in a date picker, a data grid, an autocomplete with virtualised options, and have it working before lunch. You aren't reinventing the wheel. For a huge amount of software that is exactly the right call, because the complex, fiddly, accessibility-laden components are genuinely hard to build well, and a mature framework has years of edge-case fixes baked into it that you won't reproduce in an afternoon.
If your job is to ship internal tooling quickly, or you're a small team without the appetite to own a component layer, frameworks earn their keep. I'm not anti-framework. I'm anti-using-one-past-the-point-where-it-starts-fighting-you.
Where the model breaks down
The trouble is that the convenience and the constraint are the same thing. A component you didn't write is a component you don't fully control. As long as your design lives inside the set of options the API anticipated, everything is wonderful. The moment someone asks for something just outside that set — and in my experience they always, eventually, do — you're negotiating with somebody else's abstraction instead of writing the code you actually need.
A few years ago I led a frontend off MUI and onto a custom layer built over shadcn-style primitives, at a startup where I owned most of those calls. The trigger wasn't ideology. It was that we kept hitting that same wall, over and over, on the components most central to the product, and the workarounds had quietly become more code than the components would have been if we'd just owned them outright.
What copy-paste actually changes
This is the part that sounds like a downgrade until you've lived with it. With shadcn you don't install a <Button> from a package. You run a command, and the component's source lands in your repo, as your code, styled with Tailwind, sitting on top of an unstyled primitive underneath — Radix, or Base UI in the newer setups. There's no node_modules black box. The component is just a file you own.
So when the design calls for that focus ring to sit differently, you open the file and change the line. When you need to extend it, you extend it, because there's no API surface standing between you and the markup. The primitive underneath handles the genuinely hard parts — focus management, keyboard interaction, the accessibility tree — and you handle how it looks and behaves, which is the part you actually wanted control over in the first place.
The honest trade-off
None of this is free, and I'd be lying if I pretended otherwise. When you own the component, you own its bugs. Nobody is shipping you a fix for the edge case you haven't found yet. You don't get the enormous, battle-tested data grid for nothing. Adopt this model across a large surface and you've signed up to maintain a small component library, which is real, ongoing work.
What you get in return is that nothing is ever off-limits. No afternoon lost to a theme override, no !important you're ashamed of, no reading library source to find a class name. The cost moves from "fighting the abstraction every time the design wants something specific" to "maintaining code you can actually read." For a product where the interface is the differentiator, I'll take the second cost every time.
Where I'd still pick the framework
I don't want to leave this sounding absolute, because it isn't. If I needed a spreadsheet-grade data grid with frozen columns and virtualised rows tomorrow, I wouldn't hand-roll it over a primitive just to make a point. I'd reach for the mature thing and get on with my life. Same goes for any genuinely complex, deeply-specced component where the framework's years of edge-case work clearly outweigh the control I'd be giving up.
The judgement call comes down to one question: how central is this component to the thing that makes your product yours? The closer it sits to the core experience, the more the control is worth, and the more an afternoon lost to an abstraction actually costs you. For the components at the heart of the product, I want to own the code. For the ones around the edges, I'm perfectly happy to let someone else.