Tailwind 4 buried every utility inside a named cascade layer. Anything a component kit ships outside a layer now floats above the whole stack and wins — silently, regardless of specificity, source order, or layer order. Four kits, one stratigraphic column, and one bedrock problem nobody's kit can fix.
With Tailwind 4 non-negotiable, the styling axis ranks cleanly. It is not a quality ranking — it is a how much cascade negotiation do you sign up for ranking.
enableCssLayer on every provider, and still ships Emotion's runtime beside Tailwind. [8]Pigment CSS — MUI's zero-runtime escape from Emotion — is officially on hold in alpha [2]. MUI v9 still runs on Emotion and lists "remove dependency on Emotion" under What's next [3].
And the real Tailwind 4 tax on this stack is not the kit at all — it's Nx.
@tailwindcss/vite only scans the app directory, so classes in libs/** emit nothing until you
hand-write @source directives — and Nx has publicly decided not to build native Tailwind 4 support
[110][116].
Tailwind v4's entire entry stylesheet is four lines [98] ⭐ 96k. Read them as a cross-section — top of the file is the bottom of the column.
@layer theme, base, components, utilities; @import './theme.css' layer(theme); /* @theme tokens → CSS vars */ @import './preflight.css' layer(base); /* the reset */ @import './utilities.css' layer(utilities); /* every utility */
styles.css, default)
@layer · wins anyway ↓
p-0, m-2, size-9
@theme tokens → CSS vars
libs/**, none of the above is generated at all → see stratum 05utilities sits on top of base and preflight can't touch your utilities.className="p-0" on the kit's button does nothing.
Per the CSS cascade spec, a normal (non-!important) declaration outside any layer beats a normal declaration inside
any layer — regardless of specificity, source order, or layer order. Only !important or a different origin reverses it
[99]. This worked in Tailwind v3, which had no layers — which is
why almost every MUI-plus-Tailwind answer online diagnoses the wrong cause. Under v3 the folk wisdom was "Emotion's specificity beats my utilities,
add !important". Under v4, specificity is not the mechanism; the layer boundary is, and cranking specificity won't help.
A Tailwind collaborator confirms it in the Mantine bug thread: "Tailwind utility classes are now in cascade layers, which have lower precedence than rules not in any layer" [74] ⭐ 96k. It is not MUI-specific — Livewire's Flux kit hit the identical bug [33] ⭐ 954 — and the December 2024 thread "Tailwind 4.0 not usable with any system that provides its own styles" is still unanswered by the core team [26], as is its July 2026 successor [25].
The rest of this report is really one question: does your kit make fix (a) available, and how much ceremony does it cost?
@import "lib.css" layer(lib);
@layer theme, base, lib,
components, utilities;
@import-able at all. Emotion is not. [100]/* expand the import, drop layer() on utilities.css */
@import "tailwindcss" important; /* per-class is now a SUFFIX: */ class="bg-red-500!"
Tailwind v4's reset zeroes margins and padding, sets box-sizing: border-box; border: 0 solid, unstyles h1–h6,
and puts list-style: none on lists [30]. It lives in @layer base — the
second slab from the bottom. So once a kit is in a layer above base, preflight is a non-issue and you should not disable it;
the layer order already subsumes the conflict. Preflight only bites when the kit is unlayered, and an unlayered kit was already winning anyway.
corePlugins: { preflight: false } in v4 — corePlugins is gone entirely
[29]. Every "disable preflight to fix your component library" post older than 2025
is now wrong on the mechanism and on the config. Also newly breaking: default border colour changed from gray-200 to
currentColor, placeholders are text-colour at 50%, and button gets cursor: default.
Same column, four kits. The band above the boundary is the danger zone: anything sitting there wins the cascade without asking.
styles.layer.css. Swap the import, declare the order, done. The band drops into place between base and components.
<style> at runtime and can't be @imported, so a React provider prop is the only supported route into a layer.
Pigment CSS is dead in the water. The repo's own description reads "⚠️ Alpha phase, currently, on hold"
[1] ⭐ 1.1k. It is not archived. It is also not moving: v0.0.30 in Jan 2025 →
a 16-month gap → v0.0.31 in May 2026, a maintenance-only bump [6]. Never left 0.0.x.
MUI's January 2026 roadmap is unambiguous: resources went to Base UI instead
[2]. Treat it as unavailable. Do not plan around it.
What shipped instead: v9, still on Emotion. MUI skipped v8 entirely —
v7.0.0 (Mar 2025) → v9.0.0 (Apr 2026) → 9.2.0 (3 Jul 2026), with no 8.x on npm at all [4].
@mui/styled-engine@9.1.1 hard-depends on @emotion/cache, @emotion/serialize and @emotion/sheet
[5]. The v9 perf claims are modest: ~3% bundle reduction vs v7, up to 30% faster sx
[3].
But the cascade problem itself, MUI solved — properly, first-party, and it's good. Maintainer siriwatknp shipped
enableCssLayer on StyledEngineProvider [19], and there is now a
first-party Tailwind v4 integration page [8]:
<StyledEngineProvider enableCssLayer> <GlobalStyles styles="@layer theme, base, mui, components, utilities;" /> {/* app */} </StyledEngineProvider>
A modularCssLayers option goes further, splitting output into mui.global, mui.components,
mui.theme, mui.custom, mui.sx [9]. MUI even ships an
agent skill encoding the recipe, which additionally requires createTheme({ cssVariables: true })
[11].
enableCssLayer on any provider — SSR cache, test harness, Storybook, a lazily-mounted portal root — and utilities silently lose again. MUI's own verification step is manual: check the DevTools styles tab and confirm the mui layer comes before utilities [8].modularCssLayers is a footgun on existing apps. MUI warns it causes "unexpected changes to the look and feel of the UI" because theme overrides that previously lost now win [9]. Greenfield dodges this.guides/interoperability still prescribes injectFirst, corePlugins: { preflight: false } and important: '#root' [23] — all three dead under v4. injectFirst only reorders <head>; it is irrelevant once layers are involved.<style> tag injection… The first problem is not solvable" [108].Steelman. Infinum's frontend handbook treats coexistence as supported, boring practice: Tailwind for layout and spacing, the MUI theme for stateful styles [36]. It works. It is just the only one of the four where "does my utility class apply?" is a question you have to keep asking.
There is no cascade problem because the components carry no CSS of their own — they are Tailwind utility classes in TSX that the CLI copies into
your repo. cn() is literally twMerge(clsx(inputs)) [46].
No Emotion, no theme provider, no runtime. With one 2026 asterisk.
shadcn (v4.13.0) exports
"./tailwind.css" [130], the manual-install page now tells you to pnpm add shadcn
[119], and the May 2026 changelog is explicit: "When you run init, it adds
@import "shadcn/tailwind.css" to your global CSS file" [129].
There is a backlash thread [132].
@keyframes, nine
@custom-variant data-* declarations and a few @utility definitions — no colour tokens, no :root/.dark
block, no component rules [131]. It is Tailwind source, not competing CSS: it compiles
into Tailwind's own layers. And shadcn eject inlines it and drops the dependency in one command.
This collapses two of your four candidates into one path. "shadcn/ui" and "Base UI" are no longer distinct choices on the styling axis —
shadcn is the pre-styled Tailwind layer over Base UI. The governance backdrop is the reason the default flipped:
Radix was acquired by WorkOS and "updates have slowed for some components"
[89]. ⚠ The Radix→Base UI migration ships as an
AI skill, not a codemod — asChild becomes render, positioning moves to @floating-ui/react
[97]. Greenfield sidesteps this by starting on Base UI.
cva package, still at 1.0.0-beta.7 after 2+ years of beta ⭐ 6.9k. The alternative is tailwind-variants 3.2.2 [51].Because shadcn's code lives in your repo, swapping any of these out is a local refactor, not a fork. That is the copy-in model's actual dividend.
The HN thread on the Base UI default (5 Jul 2026, 282 points, 165 comments) is a live referendum on the copy-in model [58]:
The repo carries 2,108 open issues against 119k stars [63]. On Tailwind-4-specific pain the representative issue is the HSL→OKLCH conversion wrecking custom themes — closed as not-planned [60].
Mantine cooperates with layers out of the box. Every @mantine/* package ships a parallel styles.layer.css
wrapping all rules in @layer mantine [69]. The whole fix is three lines
[75]:
@layer theme, base, mantine, components, utilities; @import 'tailwindcss'; @import '@mantine/core/styles.layer.css';
Layer order does all the work: mantine sits after base (so preflight can't nuke Mantine) and before
utilities (so className="p-4" on a Mantine Button wins). You do not disable preflight — the layer ordering subsumes it.
Architecture has been stable since v7 (2023); the 8→9 migration guide lists no breaking changes to PostCSS or cascade layers
[68].
PostCSS vs @tailwindcss/vite: they compose. This is the question most likely to worry an Nx+Vite team, and the answer is
reassuring. Vite auto-applies any valid postcss.config.* to all imported CSS, independent of which Vite plugins are loaded
[105]. So @tailwindcss/vite (Lightning CSS, no PostCSS) runs its own pass while your
postcss.config.cjs with postcss-preset-mantine runs on your CSS modules. Pick one Tailwind integration —
the Vite plugin or @tailwindcss/postcss, never both [106].
@theme without @reference [104]. Mantine's model is CSS Modules. Keep them disjoint and you're fine; want Tailwind utilities inside your own .module.css and you're in the configuration Tailwind explicitly discourages.@theme in one @import [77]. Actively maintained — but single-maintainer at 176 stars is a real bus-factor risk for a production admin console.classNames prop targets every inner element [80] — that is the seam through which Tailwind utilities get injected. Theming is pure CSS variables [79]. Zero runtime.v1.0.0 shipped 11 December 2025 with 35 unstyled components and a package rename to @base-ui/react
[14]. Cadence since is roughly monthly, up to 1.6.0
[96]. The team narrative checks out —
"From the creators of Radix, Material UI, and Floating UI" [83] — and MUI has
confirmed MUI Base is deprecated in its favour [2].
The Tailwind 4 fit is unusually clean, and needs no plugin. Tailwind 4 supports bare boolean data variants —
data-open:, data-disabled:, data-highlighted: map 1:1 onto Base UI's flag attributes — plus
data-[side=top]: for valued ones [87]. Positioner variables drop straight in:
max-h-[var(--available-height)]. ⚠ Caveat: base-ui.com's own component docs use CSS Modules, not Tailwind; only the styling handbook
page shows a Tailwind example [84].
The cost is real and unavoidable. You write every button, every dialog, every tooltip — no CSS whatsoever, no colour schemes, no themes, no pre-built patterns. Concretely: Combobox has 26 separately styleable parts (Root, Input, InputGroup, Chips, Chip, ChipRemove, List, Portal, Backdrop, Positioner, Popup, Arrow, Status, Empty, Collection, Row, Item, ItemIndicator, Group, GroupLabel, Separator…) [85]. And there is no Table or DataGrid in the component list at all.
Which is exactly why you shouldn't take Base UI bare. The pre-styled Tailwind layer over Base UI already exists, is maintained, and is now mainstream: it's shadcn/ui. "Base UI vs shadcn/ui" is a false binary in July 2026 — shadcn ships the same Tailwind class layer over both primitives, and "the components look and behave the same way. Only the underlying implementation changes." [88]
| Primitive | Stars | Health, Jul 2026 |
|---|---|---|
| Base UI | ⭐ 10k | 1.6.0 · monthly releases · pushed 2026-07-14 |
| Radix Primitives | ⭐ 19k | Still active · pushed 2026-07-13 but acquired by WorkOS; updates have slowed for some components |
| Headless UI | ⭐ 29k | ⚠ Last push 2026-04-13 — 3 months stale |
| Ark UI | ⭐ 5.3k | Active · multi-framework |
| React Aria Components | ⭐ 16k | Active inside adobe/react-spectrum |
Everything above assumes the CSS exists. On an Nx monorepo, it may not. Tailwind 4 replaced content: [...] globs with automatic
detection — and with @tailwindcss/vite, the scan root collapses to the app directory.
Classes that appear only in libs/** emit zero CSS.
Nx's own React guide says it outright: "Tailwind CSS v4 automatically detects classes from source files. However, with the Vite plugin, scanning only covers the app directory" [110]. Nx's blog is blunter: "styles defined in your packages won't be included in the final bundle, leading to missing styles and broken layouts" [111]. The upstream issue has been known since March 2024 [112] and recurs constantly — including the nastiest variant: utilities present in dev, missing in the production build [114].
/* apps/admin/src/styles.css — @source paths are relative to the CSS FILE, not the CWD */ @import 'tailwindcss'; @source "../../../libs/ui"; @source "../../../libs/shared";
@source lists rot as the graph grows. The answer is
@juristr/nx-tailwind-sync (0.0.9, Feb 2026), an Nx sync generator that walks the
project graph and rewrites the @source block on nx build — a sole-maintainer, 0.0.x package on the critical path of your build.
package.json ("./styles": "./dist/styles/index.css") and the app must import it, or nothing
applies at all [111]. Libs using @apply in their own stylesheets need
@reference [110].
@property [127] and color-mix()
[125]. Fine for an internal admin console — unless the client fleet is
locked to older Safari. MUI and Mantine carry no such floor on their own.
An admin console needs a theme toggle, and the kit docs mostly duck it. Tailwind 4 has no built-in class-based dark mode — you declare the variant yourself [133]. Every kit must then be wired to the same signal, or you get two half-working toggles.
| Kit | Signal it keys off | Wiring to share one toggle | Steps |
|---|---|---|---|
| shadcn/ui | .dark on <html> | None needed — @custom-variant dark (&:is(.dark *)); is plain Tailwind, and shadcn's tokens override under the same .dark [141] |
0 |
| Base UI | ✗ none — no CSS, no theme | Nothing to sync | 0 |
| Mantine | data-mantine-color-schemeon <html> — not configurable | ⚠ Invert it — bend Tailwind to Mantine:@custom-variant dark (&:where([data-mantine-color-scheme=dark], [data-mantine-color-scheme=dark] *)); [140]Community-sourced. Not on mantine.dev. |
1 |
| MUI | prefers-color-schemeby default | ⚠ Must set cssVariables: { colorSchemeSelector: 'class' } → emits .light/.dark [134]; and InitColorSchemeScript's attribute must match [136] |
2 |
colorSchemeSelector step is a trap you find by hitting it.
The symptom is on file: "Is there any way to apply the Tailwind's dark: class prefix?"
[137], and a toggle that fails to update [data-mui-color-scheme], mis-styling everything
[138].
| Kit | Stars | Ships CSS? | Where it lands | Fix required | Runtime cost |
|---|---|---|---|---|---|
| shadcn/ui | ⭐ 119k | ✗ — the components are Tailwind classes | utilities — same layer as everything else | None | Zero |
| Base UI | ⭐ 10k | ✗ — zero CSS | n/a — you own all of it | None | Zero |
| Mantine | ⭐ 31k | ✓ CSS Modules, pre-compiled | ✗ unlayered by default → beats utilities ✓ @layer mantine via styles.layer.css |
3-line layer declaration | Zero |
| MUI | ⭐ 98k | ✓ Emotion, injected at runtime | ✗ unlayered → beats utilities ✓ @layer mui via enableCssLayer |
Provider flag on every entry point + layer string + cssVariables: true |
⚠ Emotion runtime; React discourages runtime <style> injection |
@tailwindcss/vite, no PostCSS [54]. An @→./src alias, paths in both tsconfig.json and tsconfig.app.json.@tailwindcss/vite plus a postcss.config.cjs — Vite runs both [105].@tailwindcss/vite, plus provider config in React, plus a live <GlobalStyles> layer declaration [8].@import, so Sass/Less/Stylus are out entirely [104]. The Vite plugin exists because it skips PostCSS: full build 378ms → 100ms, incremental-no-new-CSS 35ms → 192µs [101]. Custom utilities are now @utility name {} [102].On the styling axis alone, shadcn/ui on Base UI is the answer. Not because the others are broken — MUI and Mantine both have working Tailwind 4 recipes — but because it is the only option where the recipe is "nothing": no layer declaration, no provider flag, no colour-scheme selector, no reset negotiation.
Base UI has no DataGrid, and an admin console needs one [85]. This is the single likeliest reason to overturn the recommendation: MUI X's DataGrid is the field's strongest, and if the console leans on it you may choose to pay the enableCssLayer tax deliberately.
The copy-in model is genuinely contested, and 2026 muddied it: shadcn now ships a real npm dependency with a real CSS file [130], and "upgrade" is an AI skill rather than a version bump. shadcn eject buys the old model back [129].
Two solo-maintained packages sit on the recommended path — tw-animate-css ⭐ 781 and a 20-month-stale cva. Both are replaceable locally because the code is yours.
Batteries-included components without Emotion: zero runtime, a 3-line layer declaration, and a classNames Styles API that takes Tailwind utilities on every inner element [80]. Its weakness is documentation, not architecture.
Do not plan around Pigment CSS [2]. Do not follow any MUI+Tailwind guide that mentions corePlugins or injectFirst [23].
Do pin tailwind-merge v3 on day one [43], and wire @source before you write a single component [109].
The load-bearing citations — the ones the column is built on. The full ledger runs to 141.
@layer: unlayered normal declarations beat layered onesdeveloper.mozilla.org · official
enableCssLayer)mui.com · official
modularCssLayers, and its "unexpected changes" warningmui.com · official
@import "shadcn/tailwind.css" — and eject undoes itui.shadcn.com · official
shadcn/tailwind.css — ~16 KB, no component rulescdn.jsdelivr.net · official
styles.layer.css for every packagemantine.dev · official
@layer base — which is why layering subsumes ittailwindcss.com · official
corePlugins is gone entirelytailwindcss.com · official
@source escape hatchtailwindcss.com · official
@juristr/nx-tailwind-sync 0.0.9 — a 0.0.x package on your build's critical pathnpmjs.com · official
<style> tag injection"react.dev · official
postcss.config.* to all imported CSS — so PostCSS and the Tailwind plugin composevite.dev · official
@theme; MUI and Mantine keep a JS theme object as the source of truth.
recon
Nx 21 build ergonomics: which kit survives a shared itenium-ui library
shadcn's copy-in model does work in a shared Nx library — but only if you own the plumbing.