DEV Community

hamed pakdaman
hamed pakdaman

Posted on

Building a themeable CMS admin with shadcn/ui + Tailwind v4 — lessons from 50+ components

I shipped a Laravel CMS where the entire admin is built on shadcn/ui — 50+ components, 183 pages, three themes, runtime switching with no build step. Here are the five things that mattered most.

Why shadcn over Material UI / Ant Design

The deciding factor: shadcn components are my code. When I need to change a Button variant or extend DataTable, I edit the file. No npm overrides, no className wars, no vendor PR queue.

Tradeoff: when shadcn upstream ships a new pattern, I port it manually. Budget ~1 day/quarter for upgrades.

Lesson 1: Layout primitives save 100 pages of CSS

Three components used everywhere — PageContainer, Card, Stack. The PageContainer alone replaced ~40 page-level layout decisions. Three good ones beats 10 flexible ones.

Lesson 2: Tailwind v4 + CSS variables = themes without a build

@theme {
  --color-primary: oklch(0.55 0.27 262);
}
[data-theme="purple"] {
  --color-primary: oklch(0.55 0.27 304);
}
Enter fullscreen mode Exit fullscreen mode

Switching themes:

document.documentElement.dataset.theme = 'purple';
Enter fullscreen mode Exit fullscreen mode

No bundle changes. No re-render. CSS variables flip and every shadcn component updates because they reference the variable, not a hardcoded color. I expected theming to be the hard part — it took an afternoon.

Lesson 3: One Sidebar component, different data

shadcn's Sidebar is flexible enough to handle admin nav and docs nav with the same component. Data varies per-page; the component is shared. This is what makes a 183-page admin feel like one app.

Lesson 4: DataTable is the most underrated shadcn component

Every list view — Posts, Pages, Media, Users, Forms — uses the same DataTable wrapper. Server-side pagination, sorting, row selection, bulk actions, search. The columns array is the only per-page logic.

Lesson 5: TypeScript strictness is non-negotiable

shadcn ships excellent types. Don't loosen them. Every page-level component declares its props type explicitly, even when "obvious."

What didn't work

  • Tiptap as the rich-text editor. Great for free text, wrong for structured fields. Rebuilt on Tiptap's foundation with a custom block model — 3 weeks I'd have skipped with better evaluation.
  • Copying all 50 shadcn components up front. Only 30 actually got used. Copy on demand.
  • Tailwind v3 → v4 mid-build. ~4 hours of breaking changes. Start on v4 today.

What I'd use shadcn for again

  • Anything that needs custom design
  • Long-lived products where the maintenance cost is justified
  • Type-safe fullstack apps

What I wouldn't use it for: throwaway prototypes (use Mantine), marketing sites (Tailwind alone is faster).


Source: https://github.com/hpakdaman/unfoldcms
Live demo: https://unfoldcms.com/demo

I'm Hamed — built UnfoldCMS because none of WordPress, Contentful, or Strapi/Payload fit my Laravel shop. Honest critique welcome.

Top comments (0)