Skip to content
Vikas Saini

Full stack engineer at Tars. I build products end to end.

<- Writing

5 min readWeb Development, Frontend

Why shadcn/ui Became My Go-To After 3 Years

Why copying components into your own codebase beats installing a UI library, what it is actually good at, and where it costs you.

UI work used to frustrate me, and not because of the CSS. It was the trade-off.

Install a component library and you move fast right up until the design calls for something the library did not anticipate. Then you are fighting prop APIs, overriding styles you cannot see, and writing selectors specific enough to win a specificity war you did not want.

Build from scratch and you get complete control, but you also get to reimplement a dropdown that closes on outside click, traps focus, and works with a keyboard. For the fourth time.

Neither is a good place to be on a client deadline. shadcn/ui is the first thing I have used that does not force the choice.


It is not a dependency

This is the part people miss, and it is the whole idea.

You do not install shadcn/ui. You run a command and it writes a file into your project:

npx shadcn@latest add button

Now components/ui/button.tsx exists in your repo. It is your file. It shows up in your diffs, your reviews, your git history. There is no package in node_modules that owns it.

The button on this site is that file. I changed its variants, its sizes, and its focus ring to match the rest of the design, and nothing pushed back, because there was nothing to push back. It is a component in my codebase that I happened to start from someone else's version.

Once that clicked, the frustration went away. Customization stopped being a workaround and became the normal thing you do.


What is actually underneath

Worth being precise here, because "copy the code in" can sound like it is just snippets.

Each component is built on two things:

  • Radix UI primitives handle behavior. Focus management, keyboard navigation, portals, ARIA attributes, controlled and uncontrolled state. The hard, boring, easy to get wrong parts.
  • Tailwind handles appearance, with variants organized through class-variance-authority.

So you are not owning an accessible dropdown from scratch. You own the styling layer on top of a primitive that is maintained, tested, and used widely. That split is why this works at all. If shadcn/ui had copied in the behavior too, every project would slowly drift into its own set of subtle accessibility bugs.

Theming runs on CSS variables rather than a config object, which means dark mode and brand colors are a handful of values in one file, not a fight with a theme provider.


Where it earns its place

I reach for it on the same kinds of work every time:

  • Admin dashboards and internal tools
  • SaaS interfaces with a lot of forms and tables
  • Client projects where the design is still moving

The common thread is changing requirements. When a client looks at a build and asks for a different layout, denser tables, or a different interaction on a modal, I am editing a file I already own instead of finding out whether the library supports it.

That turns a risky question into a small one. On freelance work, where scope moves and timelines do not, that difference is most of the value.


The honest cost

It is not free, and the tradeoffs are real.

You own maintenance. There is no upgrade path. When a component improves upstream, you do not get it by bumping a version. You go read the change and decide whether to bring it in by hand. Across twenty components that adds up.

Your codebase gets bigger. Every component is real code in your repo. Someone new has to walk past thirty files in components/ui to get to your actual application code.

It assumes you know Tailwind. Not a little. If utility classes are unfamiliar, you inherit a codebase you cannot confidently change, which is worse than the library you were avoiding.

You make the design decisions. It gives you a sensible default, not a design system. Spacing, hierarchy, and consistency are still your job, and a team without a shared sense of those will produce something that looks assembled.

If you want to install a package, get a finished look, and never think about it again, use something else. That is a legitimate thing to want.


When I would still skip it

A landing page with six components and no application behind it does not need thirty files. I will write plain Tailwind and be done.

Large teams with a real design system already have this solved, usually with their own internal library and versioning that shadcn/ui deliberately does not provide.

And if the project is going to be handed to someone unfamiliar with Tailwind and then left alone for two years, I think hard about it. Owning code is only an advantage while someone is around who is comfortable changing it.


What clients actually notice

Nothing about any of this, which is the point.

They say the app feels fast, or that it looks finished, or that it does not look like a template. They are reacting to consistent spacing, focus states that behave, and interactions that do not stutter. The library did not give me those. It gave me a starting point close enough that I had time left to get them right.

That is the real argument for it. Not that it writes your UI, but that it moves the starting line far enough forward that the last ten percent, which is the part anyone notices, is still affordable.

Good UI is not about how it looks on launch day. It is about how confidently you can change it six months later. Owning the code is the only version of this I have found that stays true.

More writing