Back to all posts

React UI Libraries: Who Owns the Styling Decides Everything

Jul 08, 2026
13 min read
React UI Libraries: Who Owns the Styling Decides Everything

The demo always goes well. That's the trap.

You're evaluating a component library. You drop in their date picker, their data table, their modal. In twenty minutes you have something that looks polished, and the decision feels made. Ship it.

The problems don't show up in the demo. They show up eight months later, when a designer hands you a spec that doesn't match the library's defaults, and you discover that changing one border radius means fighting three layers of the library's own styling system. That's when you learn what you actually signed up for.

So I've stopped evaluating UI libraries by how good the demo looks. The question I ask now is different, and it's uncomfortable:

When my design diverges from the library's defaults — and it will — who wins, me or the library?

That question splits the entire landscape cleanly. And it connects straight back to the styling decision from the previous article: once you've committed to Tailwind and a token system, a component library that brings its own styling engine isn't a neat addition. It's a second opinion arguing with your first one.

Let me walk through the four I actually consider — MUI, Ant Design, Radix, and shadcn/ui — grouped not by popularity, but by who holds the pen.

Two Families, Not Four Libraries

Before the individual names, the split that matters:

  • Batteries-included, styled libraries — MUI, Ant Design. They give you finished, good-looking components and the styling system that drives them. You adopt their look and their theming model.
  • Headless / you-own-the-styles — Radix, shadcn/ui. They give you behavior and accessibility, and hand the styling back to you.

Almost everything else follows from which family you pick. The first family gets you to "looks done" fastest. The second gets you to "looks like ours" without a fight. Neither is wrong. They're answers to different questions.

MUI: Powerful, Complete, and Opinionated

MUI (Material UI) is the library I'd hand someone who needs a lot of surface area, fast, and doesn't have a strong custom design language to protect.

import { Button, TextField } from '@mui/material'; function PolicyForm() { return ( <form> <TextField label="Holder name" variant="outlined" /> <Button variant="contained">Save</Button> </form> ); }

What's genuinely great: the component coverage is enormous. Data grids, date pickers, autocomplete, everything. The accessibility is solid. For an internal tool or a dashboard where "clean and consistent" beats "uniquely ours," MUI gets you there remarkably fast.

Here's my reservation, and it's the same one that decides most of this article. MUI brings its own styling engine — historically Emotion, a runtime CSS-in-JS system. You saw in the last article why I've cooled on runtime CSS-in-JS: the cost moved, and it sits awkwardly with Server Components. MUI inherits that tension. And it carries a point of view — Material Design — that you either want, or spend a long time theming your way out of. Overriding MUI deeply means learning MUI's override system, which is its own body of knowledge.

MUI isn't a bad choice. It's a committing choice. You're adopting a design language and a styling runtime along with the components.

Ant Design: Enterprise Density Out of the Box

Ant Design is MUI's closest sibling in philosophy, aimed at a slightly different target: dense, data-heavy enterprise applications.

If you're building an admin panel with complex tables, filters, and forms — the CRM-shaped software this series keeps circling back to — Ant's defaults are tuned for exactly that. The tables alone save weeks.

The trade-off is the same shape as MUI's, just with a different accent. You're adopting Ant's visual language, which is strong and distinctly Ant — apps built with it tend to look like they're built with it. Deep customization means working within Ant's theming system, and stepping outside its defaults gets progressively harder the further you go. Honestly, if your product needs to look like your brand rather than like an Ant demo, this is the friction you'll feel most.

Both MUI and Ant answer the question "how do I get a lot of good components quickly?" Well. The next two answer a different question: "how do I keep control of how everything looks?"

Radix: Behavior Without the Costume

Radix Primitives took the part that's genuinely hard — accessible, correct interactive behavior — and shipped only that. No visual design at all.

import * as Dialog from '@radix-ui/react-dialog'; function ConfirmDialog() { return ( <Dialog.Root> <Dialog.Trigger>Delete policy</Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/40" /> <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6"> <Dialog.Title className="text-sm font-semibold">Are you sure?</Dialog.Title> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ); }

Look at what Radix gives you and what it doesn't. It gives you the dialog's focus trapping, escape-to-close, scroll locking, ARIA wiring, portal handling — all the things that are tedious and easy to get subtly wrong. It gives you no appearance. Every className there is mine, and it's Tailwind, and Radix doesn't care.

That's the whole point. Radix is what you reach for when accessibility matters and you refuse to inherit someone else's look. The cost is honest: you build the visual layer yourself. For a one-off that's work. Which is exactly the gap the last option fills.

shadcn/ui: The One That Fits This Series

shadcn/ui isn't really a library, and that's the clever part. It's a collection of components built on Radix, styled with Tailwind, that you copy into your own codebase.

You don't npm install a black box. You run a command, and the component's source lands in your project as your code:

npx shadcn@latest add dialog

Now components/ui/dialog.tsx is yours. Radix underneath for behavior, Tailwind classes for the look, and every line editable because it lives in your repo. There's no override system to learn — if you want a different border radius, you change the border radius, because it's just a file you own.

I'll be direct about why this fits the Playbook so well. We chose Tailwind and a token system in the last article. shadcn/ui is built on exactly that foundation — its components read from your Tailwind tokens, so they inherit your design system instead of imposing one. Radix gives the behavior I'd otherwise have to build by hand. And because the code is mine, the library can never become the thing I'm fighting. There's no black box to fight with.

The trade-off is real and worth stating: you own more code. Updates aren't an npm update — you re-pull components or merge changes by hand. For some teams that ownership is a burden. For a team that wants its product to look like itself and never wants to be trapped by a vendor's styling decisions, it's the feature, not the cost.

The Decision, Honestly

Strip away the popularity contests and it comes down to that one question — who owns the styling:

  • You need enormous component coverage fast, and a strong custom look isn't the priority → MUI. Adopt its language on purpose.
  • You're building dense, data-heavy enterprise screens → Ant Design. Its defaults are tuned for exactly that.
  • You need accessible behavior but insist on owning every pixel → Radix primitives, styled yourself.
  • You want owned, Tailwind-native components with Radix behavior underneath → shadcn/ui. This is my default for the kind of app this series builds.

Notice that the "right" answer flips entirely based on one thing: how much your product needs to look like yours versus how fast it needs to look done. There's no library that wins both, and anyone selling you one hasn't hit month eight yet.

For the Playbook, I'll build on shadcn/ui, because it's the choice that agrees with every decision we've made so far instead of arguing with one of them. That coherence — styling, tokens, components all pointing the same way — matters more than any single component's polish.

Components give you the pieces. The next article is about the hardest piece to get right: forms. We'll build real ones with TanStack Form, the kind with conditional fields, async validation, and the messy requirements that make forms the place UIs go to die.

If you're weighing a component library right now, do one thing before you commit: try to restyle its busiest component to look nothing like the default. However that fight goes is your real answer. And if you've made peace with MUI or Ant at scale and found the customization ceiling higher than I'm describing, tell me — I'd genuinely like to know where the line actually sits.

Found this helpful?

Let's discuss your project needs.

Get in touch