Build1 distinct publisher3 min readUpdated
A design engineer got emotion, two legacy utility frameworks and Tailwind 4 sharing pages by rewriting selectors at build time. Three earlier attempts had been fighting the wrong layer.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
James Coombs, a design engineer, reports moving a large frontend off Material UI onto a custom design system built on Radix and Tailwind while four CSS systems ran at once in the same bundles and on the same pages [1][2]. The useful part is not the design system: three earlier approaches had been rejected on the assumption that coexistence requires component-level isolation, and nobody had tested the CSS layer [3].
The four systems were Material UI's emotion CSS-in-JS runtime, two legacy utility frameworks (one in-house, one open source) sitting alongside an in-house component library, and the new design system on Tailwind CSS 4 [2]. Priced as an isolation problem, the work was not a migration. The codebase was roughly 960 files as of April 2026, spread across the monorepo, and wrapping each component in a boundary means touching every one of them before the migration starts, which Coombs calls a rewrite [4].
The three rejected options all sat at the component level [3]. Manual class prefixing came to 70-plus files of mechanical edits, plus a rule that every new component be authored with the prefix, scaling linearly with component count [5]. Runtime class rewriting added complexity and latency to every component mount and broke down when third-party libraries generated their own class names [6]. Shadow DOM gave real encapsulation but broke React portals: dialogs, popovers, tooltips and dropdown menus render to document.body, and the boundary stops them inheriting theme tokens [7].
What worked was config. The PostCSS plugin postcss-prefix-selector rewrites every generated selector to sit under a scope class while the stylesheet is built [8]. `.flex { display: flex }` has specificity 0-1-0; `.ds-scope .flex` has 0-2-0, so the scoped rule beats the unscoped global, and by Coombs's account that is the entire mechanism [9]. Consuming components need zero changes; a `DesignSystemProvider` adds the `.ds-scope` class, everything inside gets the new system and everything outside keeps working [11].
The honest caveat is that there is no `!important`, so the win is not guaranteed. A legacy compound selector at the same 0-2-0 specificity ties, and source order decides; Coombs says one component in the codebase carries a comment recording exactly that, where a preflight rule ties with a legacy button's generated class, and that ties should be expected and hand-resolved [10].
Two details keep it standing. One pipeline runs five PostCSS plugins in sequence for the scoped build: Tailwind, a legacy-transform reset, postcss-prefix-selector, a layer-removal plugin and a preflight-scoping plugin [12]. Layer removal is the non-obvious one: stripping `@layer` puts design-system rules at normal cascade priority, because anything inside `@layer base` loses to any unlayered legacy CSS regardless of specificity [13]. Then portals: Dialog, Popover, Tooltip and DropdownMenu render outside the wrapper, so their content inherits nothing and buttons inside modals lose styling [14]. The first fix, a wrapper injecting the scope class around every portal, broke Radix UI's `SlotClone` ref forwarding and composed components silently lost their refs [15]. The working version is a React Context hook, `usePortalScopeClass`, that returns the class only inside the provider, applied through an inline conditional wrapper [16].
Worth watching: the total component-level cost came to four portal components at one edit each, against roughly 960 files under the isolation plan [17][18]. Every new portal primitive inherits that one edit [18]. And because `@layer` is stripped deliberately [13], upstream changes that assume layered output, plus any legacy CSS edit that lands a new 0-2-0 tie [10], are where this arrangement will next need attention.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
James Coombs is a design engineer who migrated a large frontend from Material UI to a custom design system built on Radix and Tailwind.
Four CSS systems had to run simultaneously in the same bundles and on the same pages without collision: Material UI's emotion CSS-in-JS runtime, two legacy utility frameworks (one in-house, one open source) alongside an in-house component library, and the new design system on Tailwind CSS 4.
At roughly 960 files as of April 2026, spread across the whole monorepo, component-level isolation would mean touching every file before migration could begin, which Coombs describes as a rewrite rather than a migration strategy.
Manual class prefixing would require 70-plus files of mechanical changes plus every new component authored with the prefix, and scales linearly with component count.
Runtime class modification, a JavaScript layer that rewrites class names at render time, adds complexity and latency to every component mount and is fragile when third-party libraries generate their own class names.
Shadow DOM gives true encapsulation but breaks React portals: dialogs, popovers, tooltips and dropdown menus all render to document.body, and Shadow DOM boundaries prevent them from inheriting theme tokens.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanism verifiable, deployment unverified
The technical core is checkable against platform semantics anyone can reproduce: selector specificity arithmetic, the @layer cascade rule, and the Radix SlotClone ref-chain constraint, all shown with concrete code. What is not evidenced is the deployment itself: one author, one post, no repository, no diff, no measurements, and no second account of the ~960-file monorepo or the team's prior rejections.
One self-reported codebase
Adoption evidence is a single disclosed in-progress migration in the author's own monorepo. There is no second team, no download or dependency figure for the scoped configuration approach, and no third-party report of using it, so adoption is real but minimal.
Headline outruns a narrow, caveated technique
Positive but modest. The framing that this is the CSS problem nobody thinks is solvable, plus the 'zero changes' formulation, overstates what is presented: a build-config trick that the author himself says buys 'almost all' of the coexistence, requires hand-resolving specificity ties, needs @layer stripped, and needed a bespoke portal hook after a first attempt silently broke refs. The body text is candid enough that the gap sits in the packaging, not the substance.
Practitioner credibility, no vendor stake
The visible incentive is professional-reputation publishing: a named design engineer writing up his own architectural decision on a developer community site. The tooling recommended is third-party open source (PostCSS, postcss-prefix-selector, Tailwind, Radix) rather than a product the author is shown to sell, and the post discloses its own failures, so commercial pressure on the claims appears low.
Moderate: sound reasoning, thin sourcing
Confidence is capped by single-source, single-author sourcing with no independent verification, while the specificity, cascade and Radix behaviour claims are internally consistent and independently checkable, and the author flags his own limits. Enough to act on as a technique to evaluate; not enough to treat the reported outcomes as established.
build
The migration guide was wrong in 10 days. 48 JSON files and a playbook replaced it.1 distinct publisher
build
Next.js's architecture is working. Its operating manual isn't keeping up.1 distinct publisher
build
A build step instead of a backend: 1,025 records, 8 locales, no runtime API1 distinct publisher
build
Tailwind v4 moves your tokens into CSS. The token name is now the API.1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 19, 2026