Tailwind CSS 66 🎨 Micro-Frontends CSS Architecture and Scope Isolation
A micro-frontend splits a large application into independently deployed pieces. Each piece is owned by a different team, built by a different pipeline, and loaded into the same page. The JavaScript is separated by the module federation or the iframe, and the CSS is the hard part. Two teams that both ship a global stylesheet will produce collisions, and a Tailwind utility that one team overrides will break the other team’s layout. The scope isolation is the discipline that keeps the pieces independent, and Tailwind’s layer system, prefix option, and CSS variable approach are the tools.
Key point: The @layer system in Tailwind v4 organizes the CSS into the layers with defined precedence. The utilities layer comes last, and the base layer comes first. A micro-frontend that respects the layer order does not fight the other frontends for the specificity, and a frontend that loads its styles into a named layer keeps them isolated from the rest.
Why the micro-frontend CSS problem exists
The global scope problem. Every micro-frontend that loads a global stylesheet contributes to the document’s style scope. Two frontends with a .button class produce the last-loaded wins. The specificity is the same, and the order is the load order.
The utility collision problem. Two frontends that both use Tailwind ship the utilities they use. A bg-blue-600 in one frontend and a bg-blue-600 in the other are the same class, and the value is the same unless one of them has customized it. The collisions are subtle: the two frontends think they share the utility, and the one that loads second overwrites the first’s custom values.
The specificity problem. A micro-frontend that wraps its styles in a .my-app prefix adds a specificity level. The wrap keeps the styles scoped, and it also means the Tailwind utilities inside have a higher specificity than the host’s. The unintended overrides are the result.
The design system problem. The host application and the micro-frontends should share the design tokens. The tokens are the custom properties, and the custom properties cross the shadow boundary. The shared tokens and the scoped styles are the combination.
The layer problem. The CSS cascade layers are the modern mechanism for the scope isolation. The @layer rule declares the order, and the styles inside a layer have the precedence determined by the layer, not the specificity. The Tailwind v4 uses the layers, and the micro-frontends that respect them compose.
The version problem. Two micro-frontends may use the different Tailwind versions. The v3 uses the @tailwind base/components/utilities directives, and the v4 uses the @import "tailwindcss". The two versions produce the different CSS, and the collisions are the result. The alignment on the version is the first step.
The bundle problem. Every micro-frontend that ships its own Tailwind bundle duplicates the base styles and the utilities. The shared bundle or the layer-based composition is the fix. The host provides the base layer, and the micro-frontends provide their own scoped layers.
a. The CSS cascade layers
The CSS cascade layers are the foundation of the micro-frontend isolation. The @layer rule declares the named layers, and the order of the declaration determines the precedence.
The layer declaration
@layer reset, base, components, utilities;
The order is the precedence. The reset layer has the lowest precedence, and the utilities layer has the highest. A style in the utilities layer overrides the same specificity style in the components layer.
The styles in the layers
@layer base {
h1 {
font-size: 2rem;
}
}
@layer utilities {
.text-sm {
font-size: 0.875rem;
}
}
The .text-sm in the utilities layer overrides the h1 in the base layer, even though the h1 selector has a lower specificity. The layer order wins over the specificity.
The Tailwind v4 layers
The @import "tailwindcss" produces the layers in the correct order.
@import "tailwindcss";
The import expands to the theme, the base, the components, and the utilities layers in the defined order. The developer adds the custom layers around them.
The custom layers
@layer reset, base, components, utilities, app;
@import "tailwindcss";
@layer app {
.my-app-button {
background: hsl(var(--color-primary));
}
}
The app layer has the highest precedence. The micro-frontend’s styles override the Tailwind utilities, which is the intent for the app-specific rules.
The layer order across the micro-frontends
The host application declares the layer order once, and the micro-frontends use the named layers.
/* The host */
@layer host-reset, host-base, mfe-a, mfe-b, host-utilities;
The mfe-a and the mfe-b are the layers for the two micro-frontends. The host controls the order, and the frontends place their styles in the assigned layers.
The layer table
| Layer | Precedence | Content |
|---|---|---|
reset | Lowest | The normalize |
base | Low | The element defaults |
components | Medium | The component classes |
utilities | High | The Tailwind utilities |
app | Highest | The app-specific |
b. The scope isolation strategies
The strategies for keeping the micro-frontend’s styles from leaking into the rest of the page.
The prefix option
The Tailwind prefix option adds a prefix to every utility.
// tailwind.config.js
export default {
prefix: 'mfe-a-',
};
The bg-blue-600 becomes mfe-a-bg-blue-600. The collision with the other frontends is impossible because the prefixes differ.
The prefix in v4
The Tailwind v4 uses the @import "tailwindcss" prefix(mfe-a) syntax.
@import "tailwindcss" prefix(mfe-a);
The mfe-a-bg-blue-600 is the utility, and the frontend’s markup uses the prefixed class names.
The @scope rule
The @scope rule limits the styles to the subtree.
@scope (.mfe-a-root) {
.button {
background: hsl(var(--color-primary));
}
}
The .button styles apply only inside the .mfe-a-root subtree. The scope is the modern alternative to the manual prefix.
The scope with the donut
The @scope supports the donut scope with the to clause.
@scope (.mfe-a-root) to (.mfe-a-exclude) {
.button {
background: hsl(var(--color-primary));
}
}
The styles apply inside the .mfe-a-root but not inside the .mfe-a-exclude subtree.
The shadow DOM
The shadow DOM is the strongest scope. The styles inside do not leak, and the styles outside do not apply. The custom properties cross the boundary, and the tokens are the bridge.
class MfeARoot extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}
The shadow DOM is the right choice when the frontend can be encapsulated in a custom element. The styling inside uses the custom properties and the adopted stylesheet.
The iframe
The iframe is the strongest isolation. The styles are in a separate document, and the collisions are impossible. The trade-offs are the layout constraints, the communication overhead, and the accessibility issues.
The strategy table
| Strategy | Isolation | Complexity |
|---|---|---|
| Prefix | The class name collision | Low |
@scope | The subtree | Medium |
| Shadow DOM | The full encapsulation | Medium |
| iframe | The document | High |
| Layer | The precedence | Low |
The strategy combination
The real applications combine the strategies. The host declares the layers, the frontend uses the prefix, and the design tokens are the custom properties.
@layer host, mfe-a, mfe-b;
@import "tailwindcss" prefix(mfe-a);
@layer mfe-a {
.mfe-a-button {
background: hsl(var(--color-primary));
}
}
c. The design tokens and the shared theme
The design tokens are the shared vocabulary across the micro-frontends. The custom properties are the mechanism.
The host theme
The host application declares the tokens.
:root {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius-md: 0.375rem;
--font-sans: system-ui, sans-serif;
}
The micro-frontend usage
The micro-frontend uses the tokens in its styles.
@layer mfe-a {
.mfe-a-button {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
border-radius: var(--radius-md);
font-family: var(--font-sans);
}
}
The tokens inherit from the host, and the micro-frontend does not need to declare them.
The token versioning
The tokens are the API. The host documents the token names and the values, and the micro-frontends depend on the contract. A token change is a breaking change, and the version is the signal.
## Design tokens v2
- `--color-primary` — The primary background
- `--color-primary-foreground` — The primary text
- `--radius-md` — The border radius
The theme isolation
The micro-frontend can override the tokens inside its subtree.
.mfe-a-root {
--color-primary: 142 71% 45%;
}
The override applies to the subtree, and the host’s theme is unchanged.
The dark mode
The dark mode is the set of the token values on the .dark class.
.dark {
--color-primary: 0 0% 98%;
--color-primary-foreground: 240 5.9% 10%;
}
The micro-frontends inherit the dark values, and the component’s CSS does not need the dark variant.
The Tailwind theme mapping
The Tailwind v4 @theme block maps the tokens to the utilities.
@import "tailwindcss";
@theme {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius-md: 0.375rem;
}
The bg-primary utility is generated, and the frontend uses it. The @theme block is the single source for the tokens.
The token table
| Token | Purpose | The host | The frontend |
|---|---|---|---|
--color-primary | The primary background | Declares | Uses |
--color-primary-foreground | The primary text | Declares | Uses |
--radius-md | The border radius | Declares | Uses |
--font-sans | The font family | Declares | Uses |
Complete Example Session
/* ============================================
PART 1: THE LAYER DECLARATION
============================================ */
@layer reset, base, components, utilities, mfe-a, mfe-b;
/* The order is the precedence. */
/* mfe-b overrides mfe-a. */
/* ============================================
PART 2: THE HOST THEME
============================================ */
:root {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius-md: 0.375rem;
--font-sans: system-ui, sans-serif;
}
/* ============================================
PART 3: THE MICRO-FRONTEND A
============================================ */
@layer mfe-a {
.mfe-a-button {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
border-radius: var(--radius-md);
font-family: var(--font-sans);
padding: 0.5rem 1rem;
}
}
/* ============================================
PART 4: THE MICRO-FRONTEND B
============================================ */
@layer mfe-b {
.mfe-b-button {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
border-radius: var(--radius-md);
padding: 0.75rem 1.5rem;
}
}
/* ============================================
PART 5: THE PREFIX OPTION
============================================ */
@import "tailwindcss" prefix(mfe-a);
/* <button class="mfe-a-bg-blue-600 mfe-a-px-4"> */
/* The prefix prevents the collision. */
/* ============================================
PART 6: THE @scope RULE
============================================ */
@scope (.mfe-a-root) {
.button {
background: hsl(var(--color-primary));
}
}
/* The styles apply only inside the .mfe-a-root subtree. */
/* ============================================
PART 7: THE THEME OVERRIDE
============================================ */
.mfe-a-root {
--color-primary: 142 71% 45%;
}
/* The override applies to the subtree. */
/* The host's theme is unchanged. */
/* ============================================
PART 8: THE DARK MODE
============================================ */
.dark {
--color-primary: 0 0% 98%;
--color-primary-foreground: 240 5.9% 10%;
}
/* The micro-frontends inherit the dark values. */
/* ============================================
PART 9: THE TAILWIND THEME MAPPING
============================================ */
@import "tailwindcss";
@theme {
--color-primary: 221 83% 53%;
--color-primary-foreground: 0 0% 98%;
--radius-md: 0.375rem;
}
/* The bg-primary utility is generated. */
/* ============================================
PART 10: THE COMPLETE PATTERN
============================================ */
/*
1. Declare the layer order in the host.
2. Assign each micro-frontend its own layer.
3. Use the custom properties for the design tokens.
4. Use the prefix option for the class-name isolation.
5. Use the @scope rule for the subtree isolation.
6. Use the shadow DOM for the full encapsulation.
7. Override the tokens in the subtree when needed.
8. Document the token contract.
9. The dark mode is the token change.
10. The layers prevent the specificity fights.
*/
The ten parts showed the layer declaration, the host theme, the two micro-frontends, the prefix option, the @scope rule, the theme override, the dark mode, the Tailwind theme mapping, and the complete pattern.
Quick Reference
The Layer Order
| Layer | Precedence |
|---|---|
reset | Lowest |
base | Low |
components | Medium |
utilities | High |
app | Highest |
The Isolation Strategies
| Strategy | Isolation | Complexity |
|---|---|---|
| Prefix | The class name | Low |
@scope | The subtree | Medium |
| Shadow DOM | The full | Medium |
| iframe | The document | High |
| Layer | The precedence | Low |
The Prefix Syntax
| Version | Syntax |
|---|---|
| v3 | prefix: 'mfe-a-' in the config |
| v4 | @import "tailwindcss" prefix(mfe-a) |
The Token Contract
| Token | Purpose |
|---|---|
--color-primary | The primary background |
--color-primary-foreground | The primary text |
--radius-md | The border radius |
--font-sans | The font family |
The Scope Rule
| Syntax | Effect |
|---|---|
@scope (.root) | The styles inside the subtree |
@scope (.root) to (.exclude) | The donut scope |
Best Practices
✅ Do This:
/* Declare the layer order in the host. */
@layer reset, base, components, utilities, mfe-a, mfe-b; /* ✅ */
/* Use the prefix for the class-name isolation. */
@import "tailwindcss" prefix(mfe-a); /* ✅ */
/* Use the @scope for the subtree isolation. */
@scope (.mfe-a-root) { .button { ... } } /* ✅ */
/* Use the custom properties for the tokens. */
.button { background: hsl(var(--color-primary)); } /* ✅ */
/* Override the tokens in the subtree. */
.mfe-a-root { --color-primary: 142 71% 45%; } /* ✅ */
/* Document the token contract. */
/* The README lists the tokens. */ /* ✅ */
/* Use the shadow DOM for the full encapsulation. */
this.attachShadow({ mode: 'open' }); /* ✅ */
❌ Don’t Do This:
/* Don't use the same layer for all the frontends. */
@layer app { ... } /* ❌ The collision */
/* Don't use the global styles without the scope. */
.button { ... } /* ❌ The leak */
/* Don't use the deep selectors across the micro-frontends. */
.host .mfe-a .button { ... } /* ❌ The coupling */
/* Don't duplicate the tokens. */
:root { --color-primary: ...; } /* ⚠️ The host declares them */
/* Don't rely on the load order. */
/* The order is not the contract. */ /* ❌ */
/* Don't skip the version alignment. */
/* The v3 and the v4 produce the different CSS. */ /* ❌ */
/* Don't forget the reduced motion. */
/* The animations cross the boundaries. */ /* ❌ */
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The class collision | The same names | Use the prefix |
| The style leak | The global selectors | Use the @scope |
| The specificity fight | The wrap adds the specificity | Use the layers |
| The token not found | The host does not declare it | Add the var() fallback |
| The version mismatch | The v3 and the v4 | Align the version |
| The bundle duplication | The full Tailwind per frontend | The shared layer |
| The dark mode broken | The tokens not updated | Set the .dark values |
Real-World Examples
1. The Layer Declaration
@layer reset, base, components, utilities, mfe-a, mfe-b;
2. The Host Theme
:root { --color-primary: 221 83% 53%; }
3. The Micro-Frontend Layer
@layer mfe-a { .mfe-a-button { ... } }
4. The Prefix
@import "tailwindcss" prefix(mfe-a);
5. The @scope
@scope (.mfe-a-root) { .button { ... } }
6. The Theme Override
.mfe-a-root { --color-primary: 142 71% 45%; }
7. The Dark Mode
.dark { --color-primary: 0 0% 98%; }
8. The Tailwind Theme
@theme { --color-primary: 221 83% 53%; }
9. The Shadow DOM
this.attachShadow({ mode: 'open' });
10. The Complete Pattern
@layer host, mfe-a, mfe-b;
@import "tailwindcss" prefix(mfe-a);
Visual
The Layer Cascade
┌────────────────────────────────────────────────────────────┐
│ THE LAYER CASCADE │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ reset ← Lowest precedence │ │
│ │ base │ │
│ │ components │ │
│ │ utilities │ │
│ │ mfe-a ← The micro-frontend A │ │
│ │ mfe-b ← The micro-frontend B │ │
│ │ host-utilities ← Highest precedence │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The later layer wins over the earlier layer, regardless │
│ of the selector specificity. │
└────────────────────────────────────────────────────────────┘
The Scope Isolation
┌────────────────────────────────────────────────────────────┐
│ THE SCOPE ISOLATION │
│ │
│ HOST │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <div class="host"> │ │
│ │ <div class="mfe-a-root"> │ │
│ │ @scope (.mfe-a-root) { .button { ... } } │ │
│ │ The styles apply here. │ │
│ │ </div> │ │
│ │ <div class="mfe-b-root"> │ │
│ │ @scope (.mfe-b-root) { .button { ... } } │ │
│ │ The styles apply here. │ │
│ │ </div> │ │
│ │ </div> │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The two .button rules do not collide. │
└────────────────────────────────────────────────────────────┘
The Token Bridge
┌────────────────────────────────────────────────────────────┐
│ THE TOKEN BRIDGE │
│ │
│ HOST │
│ ┌──────────────────────────────────────────────────┐ │
│ │ :root { │ │
│ │ --color-primary: 221 83% 53%; │ │
│ │ --radius-md: 0.375rem; │ │
│ │ } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ inherits │
│ ┌─────────────────┼─────────────────┐ │
│ │ │ │ │
│ v v v │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ MFE A │ │ MFE B │ │ MFE C │ │
│ │ var(--color-primary) │ var(--color-primary) │ var(--color-primary) │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ The tokens are the shared vocabulary. │
│ Each frontend reads them, and the host provides them. │
└────────────────────────────────────────────────────────────┘
The Prefix
┌────────────────────────────────────────────────────────────┐
│ THE PREFIX │
│ │
│ WITHOUT THE PREFIX │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MFE A: <button class="bg-blue-600"> │ │
│ │ MFE B: <button class="bg-blue-600"> │ │
│ │ │ │
│ │ The same class. The same value. │ │
│ │ Unless one customizes the color. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH THE PREFIX │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MFE A: <button class="mfe-a-bg-blue-600"> │ │
│ │ MFE B: <button class="mfe-b-bg-blue-600"> │ │
│ │ │ │
│ │ The different classes. │ │
│ │ The collision is impossible. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Cascade layers | The @layer rule for the precedence |
| Layer order | The declaration order is the precedence |
| Prefix | The class-name isolation |
@scope | The subtree isolation |
| Shadow DOM | The full encapsulation |
| Custom property | The token bridge |
@theme | The Tailwind v4 token mapping |
| Token contract | The documented API |
| Layer per frontend | The micro-frontend isolation |
Key takeaways:
- The CSS cascade layers are the foundation of the micro-frontend isolation. The
@layerrule declares the order, and the styles in the later layers override the earlier ones regardless of the specificity. The host declares the order once, and the frontends place their styles in the assigned layers. - The prefix option prevents the class-name collision. The Tailwind
prefixoption (v3) and theprefix(mfe-a)import (v4) add the prefix to every utility. The two frontends with the different prefixes cannot collide. - The
@scoperule limits the styles to the subtree. The.buttoninside the@scope (.mfe-a-root)applies only inside the.mfe-a-root. The scope is the modern alternative to the manual prefix, and it supports the donut scope. - The shadow DOM is the strongest scope. The styles inside do not leak, and the styles outside do not apply. The custom properties cross the boundary, and the tokens are the bridge.
- The custom properties are the shared vocabulary. The host declares the tokens, and the frontends use them. The frontend can override the tokens in its subtree, and the host’s theme is unchanged.
- The dark mode is the token change. The
.darkclass sets the different values, and the frontends inherit them. The component’s CSS does not need the dark variant. - The token contract is the API. The host documents the token names and the values, and the frontends depend on the contract. The token change is the breaking change.
- The version alignment is the first step. The v3 and the v4 produce the different CSS. The frontends that align on the version avoid the subtle collisions.
Remember: The micro-frontends share the page, and the CSS is the shared resource. The cascade layers are the precedence, the prefix is the class-name isolation, the @scope is the subtree isolation, and the shadow DOM is the full encapsulation. The custom properties are the shared vocabulary, and the token contract is the API. The result is an application where the independent teams ship the independent frontends, the styles do not collide, and the design tokens keep the pieces visually consistent.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!