Integrate a shared design system across independently deployed micro-frontends — managing CSS isolation, shared dependencies, version alignment, and consistent theming without build coupling.
Integrate a shared design system across independently deployed micro-frontends — managing CSS isolation, shared dependencies, version alignment, and consistent theming without build coupling.
Webpack Module Federation (or Vite's vite-plugin-federation) enables micro-frontends to share a single copy of design system components at runtime, eliminating the largest problem in micro-frontend architectures: multiple React instances.
1// Host app webpack.config.js
2const { ModuleFederationPlugin } = require('webpack').container;
3
4module.exports = {
5 plugins: [
6 new ModuleFederationPlugin({
7 name: 'host',
8 remotes: {
9 checkout: 'checkout@https://checkout.acme.com/remoteEntry.js',
10 catalog: 'catalog@https://catalog.acme.com/remoteEntry.js',
11 },
12 shared: {
13 // Singleton: only one React, one design system
14 react: { singleton: true, requiredVersion: '^19.0.0' },
15 'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
16 '@acme/react': { singleton: true, requiredVersion: '^2.5.0' },
17 '@acme/tokens': { singleton: true, requiredVersion: '^3.1.0' },
18 },
19 }),
20 ],
21};
22
23// Remote (micro-frontend) webpack.config.js
24module.exports = {
25 plugins: [
26 new ModuleFederationPlugin({
27 name: 'checkout',
28 filename: 'remoteEntry.js',
29 exposes: {
30 './CheckoutFlow': './src/CheckoutFlow',
31 },
32 shared: {
33 react: { singleton: true },
34 '@acme/react': { singleton: true, requiredVersion: '^2.5.0' },
35 },
36 }),
37 ],
38};The singleton flag ensures all micro-frontends use the host's version, preventing the double-React problem that breaks hooks.
When micro-frontends load into a shared DOM, their CSS can conflict. Three isolation strategies, from most to least isolation:
Strategy 1 — Shadow DOM (maximum isolation): Wraps the micro-frontend in a shadow root. CSS from the host cannot penetrate. Drawback: design system CSS custom properties do NOT inherit across shadow boundaries (fixed in newer browser implementations with @layer and inheriting custom properties).
1class CheckoutElement extends HTMLElement {
2 connectedCallback() {
3 const shadow = this.attachShadow({ mode: 'open' });
4 // Must inject token CSS inside shadow DOM explicitly
5 const style = document.createElement('style');
6 style.textContent = tokensCss; // import as string
7 shadow.appendChild(style);
8 const root = document.createElement('div');
9 shadow.appendChild(root);
10 ReactDOM.createRoot(root).render(<CheckoutFlow />);
11 }
12}
13customElements.define('checkout-mfe', CheckoutElement);Strategy 2 — CSS Modules (moderate isolation): Each micro-frontend uses CSS Modules with a unique prefix. Classes are scoped but global CSS (tokens) can still leak.
Strategy 3 — Namespace prefix (pragmatic): Add a micro-frontend-specific prefix to all generated class names: .checkout-button instead of .button. Fast to implement, sufficient for most teams.
The hardest challenge in micro-frontend design system integration is keeping all teams on compatible versions when teams deploy independently. Three coordination patterns:
Centralized governance: The design system team sets a quarterly upgrade window. All micro-frontend teams must upgrade in a 2-week window. Breaking change support window is 6 months. Simple but requires organizational coordination.
Version matrix: Publish and maintain a compatibility matrix showing which micro-frontend versions are compatible with which design system versions. Use semantic-release with a compatibility table in the changelog.
Lazy federation with version negotiation: Module Federation's requiredVersion with fallback allows a remote to use a newer version if the host provides an older one:
1shared: {
2 '@acme/react': {
3 singleton: false, // NOT singleton — allow version negotiation
4 requiredVersion: '^2.5.0',
5 strictVersion: false, // Use compatible version if exact not available
6 version: '2.6.1', // This remote's version
7 },
8},This enables gradual upgrades but risks subtle behavior differences between versions. Monitor for divergence with the compatibility matrix.
When micro-frontends compose at runtime, the host injects the brand theme. Because CSS custom properties cascade globally, a single data-theme attribute on the document root propagates to all micro-frontends automatically — no per-MFE theme configuration needed:
1// host/src/theme.ts — runs once at application startup
2export function initializeTheme(brand: string, colorScheme: 'light' | 'dark') {
3 document.documentElement.setAttribute('data-theme', brand);
4 document.documentElement.setAttribute('data-color-scheme', colorScheme);
5 // Load brand-specific token override CSS if not already loaded
6 const existingLink = document.querySelector(`link[data-brand="${brand}"]`);
7 if (!existingLink) {
8 const link = document.createElement('link');
9 link.rel = 'stylesheet';
10 link.href = `https://tokens.acme.com/themes/${brand}.css`;
11 link.dataset.brand = brand;
12 document.head.appendChild(link);
13 }
14}
15
16// Each micro-frontend's components use CSS custom properties
17// They automatically inherit the host's theme without any configurationThis architecture means micro-frontends are completely theme-agnostic — they use token CSS custom properties, and the host controls the theme. Adding a new brand requires zero changes to any micro-frontend.
Maintaining visibility into version drift across micro-frontends is critical for planning upgrade cycles. Automate the compatibility matrix generation:
1// scripts/mfe-compat-matrix.ts
2import { Octokit } from '@octokit/rest';
3
4const MFE_REPOS = [
5 { name: 'checkout', repo: 'acme/checkout-mfe' },
6 { name: 'catalog', repo: 'acme/catalog-mfe' },
7 { name: 'account', repo: 'acme/account-mfe' },
8];
9
10async function generateMatrix() {
11 const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
12 const matrix: Record<string, string> = {};
13
14 for (const mfe of MFE_REPOS) {
15 const { data } = await octokit.repos.getContent({
16 owner: 'acme',
17 repo: mfe.repo.split('/')[1],
18 path: 'package.json',
19 });
20 const pkg = JSON.parse(Buffer.from(data.content, 'base64').toString());
21 matrix[mfe.name] = {
22 dsVersion: pkg.dependencies['@acme/react'] ?? 'not installed',
23 tokenVersion: pkg.dependencies['@acme/tokens'] ?? 'not installed',
24 lastUpdated: new Date().toISOString(),
25 };
26 }
27
28 console.table(matrix);
29 return matrix;
30}Run this weekly and post to the design system Slack channel. Highlight any micro-frontend more than one major version behind the current release — these are upgrade risks for the next breaking change.
Audit your micro-frontend setup (or design a plan if you do not have one yet) for CSS isolation strategy and design system sharing approach. Write the compatibility matrix script for your team's repos.
Your progress is saved in this browser, on this device only.