Decision. shadcn/ui in a shared Nx lib works — but the CLI is app-shaped, so you buy it with plumbing:
components.jsoninsidelibs/itenium-ui, Tailwind 4@sourceglobs pointing at the lib, and a private registry as the update channel, since copy-in has nonpm update[1][2][5]. If you won’t own that plumbing, Mantine is the low-friction default (CSS Modules, no Emotion runtime). MUI is the only option that actively fights a Vite + Nx setup [6].
The copy-in question (the one that matters)
The shadcn CLI is monorepo-aware: it reads a components.json per workspace and routes base components into the shared package while blocks land in the app [1]. But the documented setup is Turborepo + pnpm workspaces; Nx is never mentioned. In practice you hit three things [2]:
shadcn initfails against Nx’s root layout — you must point it at the base config (TS_NODE_PROJECT=tsconfig.base.json).- Generated components import each other via path aliases, which Nx’s
enforce-module-boundarieslint rule flags even though the build is fine. - Tailwind 4 does not scan across the package boundary by default — the app’s CSS must
@sourcethe lib, or components render unstyled. This exact failure is an open-ish thread on the shadcn repo for Nx + Tailwind v4 [3] ⭐ 119k.
Two workarounds exist: @nx-extend/shadcn-ui, a generator that supports Nx ≥ 21 and writes components into a lib [4]; or a custom registry — your own registry.json served privately, which the CLI can add from. That inverts the model: itenium-ui becomes the registry, apps consume it as a normal Nx lib, and upstream shadcn changes are pulled in deliberately rather than diffed by hand [5].
⚠ Copy-in never gives you semver. Updates are re-runs of add over a dirty tree. That is the real cost, not the CLI paths.
Build / bundle behaviour
| Kit | Runtime CSS cost | Nx/Vite friction | RSC / SSR |
|---|---|---|---|
| shadcn/ui ⭐ 119k | none (Tailwind 4) | CLI is app-shaped; boundary lint noise [2] | ✓ (source in your repo) |
| Base UI ⭐ 10k | none (unstyled) | lowest — plain ESM deps | ✓ |
| Mantine ⭐ 31k | CSS Modules, zero-runtime since v7 | none | ✓ no use client for styling [8] |
| MUI ⭐ 99k | Emotion runtime (~21 kB min just for @emotion/react) [7] |
barrel imports slow Vite dev/rebuilds; icons up to 6× slower via named imports [6] | needs use client |
MUI’s fix is not a bundler plugin — it’s discipline: deep path imports (@mui/material/Button) enforced by no-restricted-imports with ^@mui/[^/]+$ [6]. Production tree-shaking is fine; the tax is dev-server cold start and rebuild time, paid on every app in the workspace.
TypeScript & caching
Nx 21 recommends package-manager workspaces + TS project references over tsconfig.base.json paths for buildable/publishable libs: typecheck memory 6.14 GB → 945 MB, cached typecheck 186 s → 25 s [9]. All four kits work under this, but the copy-in kits (shadcn, Base UI) put source inside itenium-ui, so every component edit invalidates the lib’s Nx cache entry and rebuilds every dependent app. Mantine/MUI keep that surface in node_modules — smaller cache blast radius, at the price of not owning the code.
Convergence note: as of July 2026 shadcn init defaults to Base UI, not Radix [10]. The shadcn-vs-Base-UI axis is now “do I want the copy-in layer on top”, not two different primitive libraries.