shadcn/ui in Production: 1 Year, 51 Components, 210 Admin Pages — Lessons
What 51 shadcn components and 210 admin pages teach you that no tutorial does
When you ship one component on shadcn/ui, you learn what shadcn is. When you ship 51 of them across 210 admin pages in a real production CMS used by paying customers, you learn what shadcn isn't — and what the design system actually buys you once the novelty wears off.
This is the post I wish someone had written before I started building UnfoldCMS on shadcn. A year in, here's what survived production, what we ripped out, and what we'd change if we started over today.
Disclosure: I work on UnfoldCMS. These are first-person lessons from the source tree, not a hot-take.
TL;DR — the year-one verdict
shadcn/ui is the right call for a real CMS admin, but not because of the components themselves — those are well-built but plenty of libraries are. The unlock is that shadcn isn't a library: the components live in your repo, which means design-system changes become normal pull requests instead of vendor upgrade hell. Across 51 components and 210 admin pages, the parts that worked best were the patterns you'd never read about in a tutorial: how to share state between an Inertia page and a shadcn Dialog, how to keep variant explosions out of your repo, what to do when the upstream component breaks your column widths.
Lesson 1 — "Owning the code" is the actual product
The shadcn/ui marketing line is "you own the code." That sounds like a slogan until you've eaten a vendor upgrade that broke half your admin. With shadcn:
- A change to
components/ui/button.tsxis a git commit, not anpm updateand a 200-line changelog. - Editing a component to add a
size="xs"variant is one PR, reviewed like any other code. - When upstream changes (e.g. shadcn moves to Tailwind v4, swaps a primitive), you patch on your schedule, not theirs.
On the 51 components in cms/resources/js/components/ui/ today, about 18 have local diffs from the upstream version. Most are tiny — a tweaked focus ring color, an extra prop, a removed default. None of those changes would survive a vendor upgrade with a closed-source design system. They'd all be re-applied (or lost) every quarter.
The "you own the code" promise is real and it compounds. A year in, this is the single most valuable thing shadcn gave us.
Lesson 2 — 210 pages is more variant pressure than you think
Every CMS admin needs roughly the same screens: a posts table, a post editor, a media library, a users table, settings, login, password reset, dashboards. Multiply that by every domain (categories, redirects, SEO, menus, comments, settings, roles, permissions, activity log) and you land around 200 pages. We have 210 .tsx files in cms/resources/js/pages/admin/ — verified by find cms/resources/js/pages/admin -name "*.tsx" \| wc -l.
What 210 pages does to a design system: every component gets used in shapes you didn't design for. <Card> ends up nested inside <Sheet> inside <Dialog>. <DataTable> lives inside a tab inside a settings page inside a layout. The components that survived this pressure were the ones with clean composition — no internal padding assumptions, no implicit z-index, no opinions about what wraps them.
shadcn/ui got most of this right because it ships unstyled Radix primitives + Tailwind classes — composition is the whole architecture. The components that failed earliest were the ones that baked in too much opinion: an early <PageHeader> we built ourselves with hardcoded padding broke the moment we needed it on a narrow modal page. We refactored to layout primitives + a thin <PageHeader> wrapper, and it stopped breaking.
Lesson 3 — Variant explosion is the bigger risk than upstream drift
The naive read of "you own the code" is "I can add any variant I want." After about month three, our <Button> had nine sizes and seven variants. By month six we'd cut it back to five sizes and four variants — and that simpler version covers every real use case in 210 pages.
The pattern that worked: resist adding a variant until the same shape appears three times. The first time you need a button slightly different, override it with className. The second time, ditto. The third time, that's signal — add the variant. Anything below that threshold lives as a one-off and gets cleaned up later (or never, but at least it doesn't pollute the component API).
Without this discipline, "owning the code" becomes "owning a snowflake design system." That's the worst of both worlds.
Lesson 4 — Tailwind v4 was a forcing function we needed
We migrated to Tailwind v4 mid-2025, mid-build. It's the closest we came to regretting the shadcn choice. The migration touched every component, broke the theme system in unexpected ways (CSS variables instead of theme() config), and ate two weeks of work.
What we got on the other side:
- CSS-variable theming. Three production themes ship today (default blue, purple, unfold soft-purple). Adding a fourth is one file.
- Sub-100ms style recompiles in dev — the v4 oxide engine is genuinely faster than v3 in our setup.
- Zero
@applyin component code — v4 nudged us away from@applytoward composition, which made components more portable.
If we'd been on a closed-source design system, the Tailwind v4 migration would have been blocked by the vendor's timeline. Because shadcn = owned code, we shipped on ours.
See Tailwind v4 + shadcn/ui: Building a Themeable CMS for the longer write-up on what changed.
Lesson 5 — Forms are where the design system pays for itself
A CMS is half forms. Post editor, settings, user create, category create, redirect create, menu builder — every interaction either is a form or contains one. Our form stack:
Form+FormField+FormItem+FormLabel+FormControl+FormDescription+FormMessage(shadcn) on top ofreact-hook-form+ Zod.
The shadcn Form primitives bind to react-hook-form so cleanly that we don't write field-level state code anymore. One schema declaration, one auto-bound input, error messages render automatically. Across the 210 pages, every form uses this same pattern, which means a developer who's seen one form has seen them all.
This is the part of shadcn that pays back the loudest. If you're picking a design system for a CMS and you don't try the form story before committing, you're picking blind.
Lesson 6 — The DataTable is the hardest component
Our <DataTable> is built on @tanstack/react-table + shadcn's <Table> primitive. It powers the posts table, users table, categories table, redirects table, menus table, comments table, SEO table — 11 distinct production tables. After a year:
- Column resize, row reorder, multi-sort, server-side pagination, server-side filtering all work, but each one took multiple revisions to get right.
- The cleanest pattern was column definitions as data, not JSX. A column is a plain object with
accessorKey,header,cell,enableSorting. JSX lives only inside thecellfunction. This made it possible to share column definitions between admin pages without prop-drilling. - The hardest bug we hit was virtual scrolling colliding with shadcn
<Sheet>on mobile — closed scroll containers compete with the body scroll lock. Fix: render the Sheet via portal, not inline.
If we'd had to ship this on a closed-source table component, we'd have eaten the limitations. With shadcn + TanStack Table, the abstraction is ours — we extended it three times to fit the CMS shape.
Lesson 7 — Inertia 2 + shadcn was the right pairing
We use Inertia.js to render React pages from Laravel. shadcn assumes a React app but doesn't care whether it's Next.js, Vite, or Inertia under the hood. The combination of:
- Laravel routes + controllers + Eloquent on the back
- Inertia for the data bridge
- React + shadcn + Tailwind on the front
is what made a monolith with 51 components and 210 admin pages possible without a separate Node service. One deploy, one database, one repo. The Inertia model (server-side data, client-side rendering) sidesteps the API ceremony of headless setups.
For the long version of the stack argument, see Laravel + React + shadcn/ui: The Modern CMS Stack.
Lesson 8 — Things we'd do differently
If we started over today, knowing what we know now:
- Start with the lightest possible shadcn install — just
button,input,card,dialog. Add the rest only when a feature needs it. We over-added early; many of our first-month installs sat unused for months before being adopted. - Lock the design tokens before adding the second component. We changed the default border radius three times in month one. Each change cascaded through every existing component.
- Build the DataTable second, not eighth. It's the hardest component and the most important; punting on it lets ad-hoc tables proliferate.
- Add Storybook earlier. We added it month six. It would have caught several variant collisions in month two.
- Treat shadcn upgrades like dependency upgrades, not framework upgrades. When upstream changes a component, we review the diff like any PR. When we'd been treating shadcn as "set it and forget it," we missed useful upstream improvements for months.
Lesson 9 — What did NOT survive production
For balance, here's what we removed:
- Custom theme switcher with five themes. We shipped five themes (blue, purple, green, slate, unfold). Three got used. Two were vanity. Cut to three.
- A bespoke
<EmptyState>component. Got replaced by one<Card>with a centred heading +<Button>because that's what every empty state actually needed. - A
<PageHeader>with breadcrumbs + tabs + actions baked in. Too rigid. Replaced with layout primitives — a<header>div with whatever combination the page needed. - Three different toast positions (top-right, bottom-right, top-center). One position survived: bottom-right. The others created confusion across pages.
The pattern: over-engineered components didn't survive; primitives + composition did. This matches what shadcn's design philosophy advocates — but it took us a year to internalise the lesson at the design-system level, not just the component level.
People Also Ask
Is shadcn/ui ready for production?
Yes — UnfoldCMS runs 51 shadcn components across 210 admin pages in production with paying customers, no critical bugs traced to the component layer in 12 months. The patterns that scale (composition over configuration, owning the code, Radix primitives) are exactly the patterns shadcn was designed around.
How many shadcn components does a real CMS use?
We use 51 in UnfoldCMS, counted with find cms/resources/js/components/ui -name "*.tsx" \| wc -l. About 18 of those have local modifications from upstream — small tweaks like prop additions, focus-ring colors, removed defaults.
What's the hardest part of building a CMS on shadcn/ui?
The DataTable. CMS admins live and die by their tables — posts, users, categories, redirects, menus, comments. Building a single DataTable that handles column resize, row reorder, server-side pagination, multi-sort, and works inside a mobile Sheet took the longest of any component. See 50 shadcn/ui Components in a Real Production Admin for the component-by-component breakdown.
Did Tailwind v4 break shadcn/ui?
Migrating mid-build was painful — two weeks of work, every component touched. After the migration, the new CSS-variable theming and faster recompiles paid back the investment. If you're starting fresh today on Tailwind v4 + shadcn, it's smooth. Migrating an existing v3 codebase takes real work.
Would you build on shadcn/ui again?
Yes. Not because the components are better than alternatives (they're well-built but not uniquely so) — because owning the code, the form story, and the composition discipline that shadcn enforces compound over time. See The CMS Built on shadcn/ui: Why It Matters for the why-it-matters framing.
Bottom line
One year of building a CMS on shadcn/ui taught us that the win isn't the components — it's the architecture they push you toward. Composition over configuration. Owned code over vendor dependency. Forms as primitives, not widgets. Tables as data, not JSX.
If you're picking a design system for a real production admin in 2026, build a small prototype on shadcn before committing. Ship one full form, one full table, one full settings page. If those three work, the rest of the 210 pages will work too.
Want to see the production result? Try the UnfoldCMS demo — the same 51 components and 210 admin pages, live.
Sources and methodology
- Component count —
find cms/resources/js/components/ui -name "*.tsx" \| wc -l= 51. Verified at write time. - Admin page count —
find cms/resources/js/pages/admin -name "*.tsx" \| wc -l= 210. Verified at write time. - Stack — Laravel 12, React 19, TypeScript, Inertia 2, shadcn/ui, Tailwind v4, Lucide React. Confirmed in
cms/composer.jsonandcms/package.json. - Lessons drawn from internal build logs and commit history of the UnfoldCMS repo, April 2024 through June 2026.
Share this post: