PatternsMicrofrontends
CSS Isolation in Microfrontends
Techniques to prevent CSS conflicts between microfrontends
CSS conflicts are one of the biggest challenges in microfrontend architectures. This guide covers proven isolation techniques.
🎯 The Problem
/* Header MFE */
.button {
background: blue;
padding: 10px;
border-radius: 4px;
}
/* Product MFE (loaded later) */
.button {
background: red; /* ❌ Overwrites Header's blue! */
padding: 20px; /* ❌ Changes Header's padding! */
border-radius: 8px;
}
Result: All buttons become red with 20px padding 😱🛡️ Solution 1: CSS Modules (Recommended)
Automatically scope CSS to component.
How It Works
/* Header.module.css */
.button {
background: blue;
padding: 10px;
}
/* Generates unique class name: */
.Header_button__a1b2c3 {
background: blue;
padding: 10px;
}// Header.tsx
import styles from './Header.module.css';
export function Header() {
return (
<button className={styles.button}>
{/* Renders: <button class="Header_button__a1b2c3"> */}
Click me
</button>
);
}Webpack Configuration
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]',
},
},
},
],
},
],
},
};Global Styles with Modules
/* Header.module.css */
.container {
padding: 20px;
}
/* Use :global() for global selectors */
:global(.theme-dark) .container {
background: #333;
}
/* Or export for use elsewhere */
:export {
primaryColor: #0070f3;
}Pros:
- ✅ Zero runtime overhead
- ✅ Works with any bundler
- ✅ Type-safe (with TypeScript)
- ✅ Build-time scoping
Cons:
- ⚠️ Need build setup
- ⚠️ Can't style 3rd party components easily
🎨 Solution 2: CSS-in-JS
Write CSS in JavaScript for runtime scoping.
Styled Components
import styled from 'styled-components';
// Each MFE can have its own styled button
const Button = styled.button`
background: blue;
padding: 10px;
border-radius: 4px;
&:hover {
background: darkblue;
}
`;
export function Header() {
return <Button>Click me</Button>;
}
// Generates unique class: .sc-bdVaJa dqwXYZWith Theme
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#0070f3',
secondary: '#ff4081',
},
spacing: {
small: '8px',
medium: '16px',
large: '24px',
},
};
const Button = styled.button`
background: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.medium};
`;
export function HeaderMFE() {
return (
<ThemeProvider theme={theme}>
<Button>Themed Button</Button>
</ThemeProvider>
);
}Emotion
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = css`
background: blue;
padding: 10px;
border-radius: 4px;
&:hover {
background: darkblue;
}
`;
export function Header() {
return <button css={buttonStyle}>Click me</button>;
}Pros:
- ✅ Full runtime isolation
- ✅ Dynamic theming easy
- ✅ No naming conflicts
- ✅ Collocated with component
Cons:
- ⚠️ Runtime overhead
- ⚠️ Larger bundle size
- ⚠️ Flash of unstyled content
🌲 Solution 3: Shadow DOM
Browser-native isolation.
Implementation
class HeaderComponent extends HTMLElement {
constructor() {
super();
// Create Shadow DOM
const shadow = this.attachShadow({ mode: 'open' });
// Styles are scoped to shadow root!
shadow.innerHTML = `
<style>
.button {
background: blue;
padding: 10px;
}
</style>
<button class="button">Click me</button>
`;
}
}
customElements.define('app-header', HeaderComponent);With React (react-shadow)
import root from 'react-shadow';
export function Header() {
return (
<root.div>
<style>{`
.button {
background: blue;
padding: 10px;
}
`}</style>
<button className="button">Click me</button>
</root.div>
);
}Loading External Stylesheets
class HeaderComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Load external stylesheet
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'https://cdn.example.com/header-styles.css');
shadow.appendChild(link);
shadow.innerHTML += '<div class="header">Header Content</div>';
}
}Pros:
- ✅ Complete isolation
- ✅ Browser native
- ✅ No build tools needed
- ✅ Future-proof
Cons:
- ⚠️ Can't style from outside
- ⚠️ Inheritance issues (fonts, etc)
- ⚠️ Limited React support
📛 Solution 4: Naming Conventions (BEM)
Manual scoping through naming.
BEM Pattern
/* Header MFE - All classes prefixed with mfe-header */
.mfe-header {}
.mfe-header__logo {}
.mfe-header__nav {}
.mfe-header__nav-item {}
.mfe-header__nav-item--active {}
.mfe-header__button {}
/* Product MFE - All classes prefixed with mfe-product */
.mfe-product {}
.mfe-product__grid {}
.mfe-product__card {}
.mfe-product__button {} /* Different from header button! */Automated with PostCSS
// postcss.config.js
module.exports = {
plugins: [
require('postcss-prefix-selector')({
prefix: '.mfe-header',
transform: (prefix, selector) => {
return `${prefix} ${selector}`;
}
})
]
};/* Input */
.button {
background: blue;
}
/* Output */
.mfe-header .button {
background: blue;
}Pros:
- ✅ No tooling required
- ✅ Easy to debug
- ✅ Clear ownership
Cons:
- ⚠️ Manual discipline needed
- ⚠️ Verbose class names
- ⚠️ Easy to mess up
🎯 Solution 5: CSS Layers
Modern CSS feature for explicit layering.
/* Header MFE */
@layer mfe-header {
.button {
background: blue;
padding: 10px;
}
}
/* Product MFE */
@layer mfe-product {
.button {
background: red;
padding: 20px;
}
}
/* Define layer order in shell */
@layer mfe-header, mfe-product, mfe-checkout;Layer Priority
/* Shell App - Define global layer order */
@layer reset, base, components, utilities;
/* Header MFE */
@layer components {
.header-button {
background: blue;
}
}
/* Utilities always win (highest priority) */
@layer utilities {
.bg-red {
background: red !important; /* Only place for !important */
}
}Pros:
- ✅ Native CSS solution
- ✅ Explicit priority control
- ✅ No build tools needed
Cons:
- ⚠️ Modern browsers only (2022+)
- ⚠️ Need browser support
🔄 Solution 6: Runtime CSS Namespace
Dynamically scope CSS at runtime.
class CSSNamespace {
private namespace: string;
constructor(mfeName: string) {
this.namespace = `mfe-${mfeName}`;
}
injectStyles(css: string): void {
// Add namespace to all selectors
const namespacedCSS = this.namespaceCSS(css);
const style = document.createElement('style');
style.textContent = namespacedCSS;
style.setAttribute('data-mfe', this.namespace);
document.head.appendChild(style);
}
private namespaceCSS(css: string): string {
// Simple regex to add namespace to selectors
return css.replace(
/([^{}]+)\{/g,
(match, selector) => {
return `.${this.namespace} ${selector.trim()} {`;
}
);
}
cleanup(): void {
const styles = document.querySelectorAll(`style[data-mfe="${this.namespace}"]`);
styles.forEach(style => style.remove());
}
}
// Usage in MFE
const cssNamespace = new CSSNamespace('header');
cssNamespace.injectStyles(`
.button {
background: blue;
padding: 10px;
}
`);
// Becomes: .mfe-header .button { ... }Pros:
- ✅ Dynamic scoping
- ✅ Easy cleanup
- ✅ Works with any CSS
Cons:
- ⚠️ Runtime overhead
- ⚠️ Complex regex for edge cases
🎨 Solution 7: Utility-First CSS (Tailwind)
Use utility classes that are inherently scoped.
// Header MFE
export function Header() {
return (
<button className="bg-blue-500 hover:bg-blue-700 px-4 py-2 rounded">
Click me
</button>
);
}
// Product MFE
export function ProductCard() {
return (
<button className="bg-red-500 hover:bg-red-700 px-4 py-2 rounded">
Add to Cart
</button>
);
}
// No conflicts! Each utility is uniqueShared Tailwind Config
// shared/tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: '#0070f3',
},
},
},
};
// Each MFE extends this configPros:
- ✅ No naming conflicts
- ✅ Consistent design system
- ✅ Small bundle (purged)
Cons:
- ⚠️ All MFEs need Tailwind
- ⚠️ Large HTML classes
- ⚠️ Coordination needed
📊 Comparison Table
| Solution | Isolation | Performance | DX | Browser Support |
|---|---|---|---|---|
| CSS Modules | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | All |
| CSS-in-JS | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | All |
| Shadow DOM | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | Modern |
| BEM | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | All |
| CSS Layers | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Modern |
| Runtime NS | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | All |
| Tailwind | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | All |
💡 Best Practices
1. Combine Techniques
// Use CSS Modules for component styles
import styles from './Header.module.css';
// Use Shadow DOM for complete isolation
import root from 'react-shadow';
// Use BEM for global utilities
export function Header() {
return (
<root.div>
<link rel="stylesheet" href={styles} />
<header className="mfe-header">
<div className={styles.container}>
<button className={styles.button}>Click</button>
</div>
</header>
</root.div>
);
}2. Reset Styles Per MFE
/* Each MFE starts with reset */
.mfe-header {
/* Reset box-sizing */
*, *::before, *::after {
box-sizing: border-box;
}
/* Reset margins */
* {
margin: 0;
padding: 0;
}
/* Now apply MFE styles */
.button {
/* ... */
}
}3. Shared Design Tokens
// shared/design-tokens.ts
export const tokens = {
colors: {
primary: '#0070f3',
secondary: '#ff4081',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
fonts: {
body: '"Inter", sans-serif',
heading: '"Poppins", sans-serif',
},
};
// Use in CSS Modules
// Header.module.css
.button {
background: var(--color-primary);
padding: var(--spacing-md);
}
// Inject tokens as CSS variables
const root = document.documentElement;
root.style.setProperty('--color-primary', tokens.colors.primary);🏢 Real-World Examples
Spotify
- CSS Modules for components
- Shared design system
- BEM for utilities
- Shadow DOM for widgetsZalando
- CSS-in-JS (Styled Components)
- Separate theme per MFE
- Global design tokensIKEA
- Tailwind CSS
- Shared config
- Utility-first approach📚 Key Takeaways
- CSS Modules for most projects (best balance)
- Shadow DOM for complete isolation
- CSS-in-JS if dynamic theming needed
- BEM as fallback (no build tools)
- Always have a naming strategy
- Test isolation - load MFEs in different orders
- Share design tokens - consistency across MFEs
Choose based on your team's needs, not hype. CSS Modules work for 90% of cases.