shadcn/ui and the case for owning your components
Why copy-into-your-repo beats another npm dependency for design-system primitives.
For years, my instinct when I needed a dropdown, a dialog, a combobox, anything with real accessibility behavior baked in, was the same instinct every React developer has: npm install a component library, import what I needed, and move on with my life. That instinct is reasonable right up until the day the design spec asks for something the library's author didn't anticipate, and you find yourself staring at a component you don't own, reading its source on GitHub to figure out which prop, if any, lets you bend it to a shape it wasn't built for.
shadcn/ui is the first thing that actually changed that instinct for me, and it did it with an idea that sounds almost too simple to matter until you've lived the alternative: it isn't a package. Running its CLI doesn't add a dependency to package.json. It copies the actual component source, a real .tsx file, straight into your own repository, built on Radix UI's unstyled, accessible primitives underneath.
The moment ownership actually mattered
I felt the difference directly building a status-severity selector for the incident detail page. I needed a dropdown where each option showed a colored dot matching the severity's color, not just plain text — a small thing, but the kind of small thing that's exactly where a black-box component library starts to hurt.
With a typical installed library, this is where you start reading documentation for a renderOption prop, hoping one exists, and if it doesn't, you're stuck choosing between an ugly workaround or forking the whole package. With shadcn/ui, the select component was just sitting in components/ui/select.tsx, in my own repo, as ordinary code:
// components/ui/select.tsx — my file, not node_modules
const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Item ref={ref} className={cn(itemStyles, className)} {...props}>
{children}
</SelectPrimitive.Item>
)
);
Adding the colored dot wasn't a workaround, or a fork, or a GitHub issue asking for a feature. It was just editing a file I already had, the same way I'd edit any other component in the app:
<SelectItem value={severity.value}>
<span className={cn("h-2 w-2 rounded-full", severity.dotColor)} />
{severity.label}
</SelectItem>
That's the entire pitch, made concrete. Not "more flexible props," not "a bigger API surface" — just: this is my code now, so of course I can change it, the same unremarkable way I can change any component I wrote myself.
What actually gets traded away
I don't think this is a strictly-better-in-every-way story, and I'd trust it less if it were. The honest tradeoff is that a real npm-installed library gets bug fixes and accessibility patches for free every time you bump a version number. Copy the source into your repo, and every one of those fixes becomes something you have to notice happened upstream and manually re-pull in yourself. For a fast-moving library with real accessibility edge cases still being discovered, that's a genuine cost, not a nitpick.
For a project the size of a solo portfolio or SignalHQ, where the component set is fixed early and doesn't churn much once it's in place, I've decided that cost is worth it. The day the design needed a colored dot in a dropdown, ownership won outright. If I were building this inside a large team maintaining dozens of shared components across many repos, I think the calculus tips the other way, and a real versioned package with a changelog starts looking like the more responsible choice, not the lazier one.
What I actually learned
I don't think the lesson is "never install a component library again" — Radix UI itself, sitting underneath shadcn/ui, is exactly the kind of dependency I'm happy to install and never touch, because its job is unstyled accessibility behavior I have no business reimplementing myself. The lesson is narrower: the right amount of ownership depends on how much you actually expect to need to change something, not on some abstract principle that owning code is always better than depending on it. Radix, I never touch, so a dependency is the right shape for it. My select component, I touch constantly, so owning the file outright is the right shape for that.
shadcn/ui didn't teach me component libraries are bad. It taught me to actually ask, for each dependency, which of those two shapes it is — and to stop reflexively reaching for npm install as if that question had only one answer.