shadcn vs. Tailwind Is a Category Error (One Runs on the Other)
92 components, 13 runtime dependencies, zero UI libraries — what hand-rolling actually cost this codebase, and the seven components shadcn would have handed over.
shadcn/ui is not an alternative to Tailwind CSS — it is built on it. Tailwind is the styling layer that turns flex, p-4 and bg-primary into CSS; shadcn/ui is a set of accessible React components, styled with those same utilities, that you copy into your own repository instead of installing. Choosing "shadcn or Tailwind" is a category error. The real question is whether you hand-roll your components or start from someone else's copy.
This storefront answered it the hard way: 92 components, 13 runtime dependencies, zero UI libraries. Here is what that actually costs and buys.
The two things being compared
| Tailwind CSS | shadcn/ui | |
|---|---|---|
| What it is | A utility-first CSS framework | A collection of React components |
| What you get | Class names that compile to CSS | .tsx files copied into your project |
| Distribution | An npm dependency | A CLI that writes source files you then own |
| Depends on Tailwind? | — | Yes |
| Depends on React? | No | Yes |
| Gives you a dialog, combobox, date picker | No | Yes |
| Gives you spacing, colour, layout | Yes | Only by using Tailwind |
Tailwind has no opinion about what a dropdown is. shadcn/ui has a very specific one, implemented over headless primitives (Radix, and more recently Base UI) that supply the keyboard handling and ARIA wiring, with Tailwind classes for the looks.
So the honest framing is: Tailwind is a given either way. The choice is between writing your own components with it, or starting from shadcn's and editing them.
What "copy, don't install" actually means
This is the genuinely interesting idea in shadcn/ui and it's an architectural choice, not a packaging detail.
A conventional component library is a dependency. You get <Dialog> from its package, you configure it through the props its authors exposed, and when your design needs something they didn't anticipate you fight the abstraction or fork it. Upgrades arrive as a version bump.
shadcn/ui inverts that. The CLI writes component source into your repo, typically under components/ui/. It's your code from that moment: edit any line, delete the parts you don't use, rename things. In exchange, upgrades are no longer a version bump — there's no package to update, and improvements upstream reach you only if you go and re-copy.
That trade is the same one this codebase made, minus the head start.
The measured cost of hand-rolling
Everything in src/components was written here. The census:
| Layer | Components | Client Components |
|---|---|---|
| Atoms | 7 | 0 |
| Molecules | 41 | 19 |
| Organisms | 33 | 5 |
| Templates | 11 | 0 |
| Total | 92 | 24 |
And the dependency list that supports them — the complete runtime dependencies block, 13 entries:
@mdx-js/loader, @mdx-js/react, @next/mdx, @supabase/ssr,
@supabase/supabase-js, @vercel/analytics, lucide-react, next,
react, react-dom, rehype-slug, remark-gfm, server-only
No Radix, no Base UI, no headless-component package, no class-variance-authority, no tailwind-merge. Styling is 38 colour tokens in a 133-line globals.css, and Tailwind utilities in the markup.
The 24-to-92 ratio is the number that would change most under shadcn. Only 26% of components here need "use client", and zero atoms do — every primitive is a Server Component. Most shadcn components are interactive by nature and ship with "use client", so adopting them tends to push the client boundary outward: a Server Component that renders a shadcn <Button> is fine, but reaching for their <Tabs> or <Dialog> makes that subtree client-rendered. Where the boundary lands, and why it's a design decision rather than an accident, is the subject of atomic design in Next.js.
What hand-rolling actually costs
The cost is not the styling. It's the behaviour you don't see until someone tries to use it with a keyboard.
This repo has had to build, and then write up, each of these separately: a focus trap, a modal, a dropdown, tabs, an accordion, a tooltip, and an accessible mega menu. Every one of those is a component shadcn/ui would have handed over on day one with its ARIA roles and keyboard handling already correct.
That is the case for shadcn, and it's strong. A dialog is easy to draw and hard to get right: focus has to move in on open and back on close, Escape closes, focus is trapped while open, background content is inert, and the whole thing needs the right roles. Skipping that work is worth real money.
What this codebase got back for paying it:
- No abstraction between design tokens and markup. The
@themeblock generates the utilities; components use them directly. There's no variant API to learn or extend. - A strict composition rule. Atoms → molecules → organisms → templates, importing downward only. shadcn's flat
components/ui/folder is deliberately unopinionated about this; it neither helps nor hinders, but it doesn't give you the structure either. - Server Components by default, including all 7 atoms.
- No transitive dependency surface. 13 runtime packages is the whole supply chain.
Which one should you pick
| If you… | Choose |
|---|---|
| Need a dialog, combobox, command palette or date picker soon | shadcn/ui — the a11y work is done and it is genuinely hard |
| Are building mostly marketing/content surfaces | Tailwind alone; most sections are a heading, a grid and a card |
| Have a design system that doesn't look like shadcn's defaults | Tailwind alone, or expect to rewrite most of what the CLI copies |
| Want minimum client JavaScript in an App Router app | Tailwind alone; watch where "use client" lands |
| Have one developer and a deadline | shadcn/ui |
| Are shipping a template others will customize | Tailwind alone — buyers inherit your dependency choices |
That last row is why this repo chose as it did: every product it sells is source other people take over and modify, so each dependency is one more thing a buyer has to understand. The bundle-versus-accessibility tradeoff, measured against this same codebase, is covered in React UI libraries.
Note what's absent from that table: bundle size as a deciding factor. shadcn components are copied into your app and tree-shaken like your own code, so you ship what you use. The old "component library bloat" argument doesn't really apply — the costs that do are the client boundary and the design-divergence tax.
They are not mutually exclusive
The most common real-world setup is both: Tailwind for layout, spacing, colour and typography; shadcn/ui for the handful of components where correct behaviour is expensive. You can also copy a single shadcn component without adopting the rest — it's source code, so taking the dialog and writing your own buttons is a normal thing to do, not a hack.
If this project were starting again with the same constraints, that's likely where it would land: hand-rolled atoms and layout, and shadcn's dialog rather than a seventh hand-written focus trap.
Mistakes and troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Which should I install, shadcn or Tailwind?" | Category error — shadcn requires Tailwind | Install Tailwind; then decide about components |
| A whole page became client-rendered after adding a component | An interactive shadcn component carries "use client", and that boundary spreads to its subtree | Push it down to the smallest leaf that needs it |
| shadcn components don't match the rest of the site | Their defaults are a real design system, not a neutral one | Edit the copied source — that's the point of copy-don't-install |
| Upstream fixed a bug but your copy still has it | There's no package to bump | Re-copy that component and re-apply your edits |
| Tailwind classes inside a copied component don't apply | Tailwind isn't scanning that path, or v4 tokens changed after the dev server started | Check the content sources; restart the dev server after @theme edits |
| Hand-rolled dropdown works with a mouse, not a keyboard | The ARIA and focus behaviour was never implemented | Either implement it properly or copy a primitive that has |
Frequently asked questions
Is shadcn/ui a replacement for Tailwind CSS? No. It's built on Tailwind and can't work without it. It replaces the components you would otherwise write with Tailwind, not Tailwind itself.
Is shadcn/ui a component library? Not in the usual sense — you don't install it as a runtime dependency. A CLI copies component source into your project and you own it from then on, including future changes.
Do I need shadcn/ui to use Tailwind well? No. This site is 92 components and 546 prerendered pages on Tailwind with no UI library. What you give up is the accessible-behaviour head start on interactive components.
Does shadcn/ui increase bundle size compared to plain Tailwind? Only by what you use — the components are your source, bundled and tree-shaken like everything else. The real overhead is the headless primitives underneath and the client boundary interactive components introduce.
Can I use shadcn/ui components in React Server Components?
You can render them from a Server Component, but interactive ones declare "use client" themselves, so that part of the tree becomes client-rendered. Presentational ones are fine either way.
Templates in this post
ASoc Remit is a digital-payments landing template built around a transactions dashboard preview, with expense-tracking and analytics blocks, a 50+ integrations marquee and a three-tier comparison pricing table. ASoc Script markets an AI copywriting product from an interactive generator hero through five writing capabilities, a 25+-language section, a filterable template gallery and three-tier pricing. ASoc Seeker is an AI keyword-research landing page with a topic-to-keywords hero, a four-benefit grid, a how-it-works walkthrough with charts, and two-tier pricing across three billing cycles.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
