Front-end Engineering Lab
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 😱

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 dqwXYZ

With 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 unique

Shared Tailwind Config

// shared/tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: '#0070f3',
      },
    },
  },
};

// Each MFE extends this config

Pros:

  • ✅ No naming conflicts
  • ✅ Consistent design system
  • ✅ Small bundle (purged)

Cons:

  • ⚠️ All MFEs need Tailwind
  • ⚠️ Large HTML classes
  • ⚠️ Coordination needed

📊 Comparison Table

SolutionIsolationPerformanceDXBrowser 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 widgets

Zalando

- CSS-in-JS (Styled Components)
- Separate theme per MFE
- Global design tokens

IKEA

- Tailwind CSS
- Shared config
- Utility-first approach

📚 Key Takeaways

  1. CSS Modules for most projects (best balance)
  2. Shadow DOM for complete isolation
  3. CSS-in-JS if dynamic theming needed
  4. BEM as fallback (no build tools)
  5. Always have a naming strategy
  6. Test isolation - load MFEs in different orders
  7. Share design tokens - consistency across MFEs

Choose based on your team's needs, not hype. CSS Modules work for 90% of cases.

On this page