docs/cookbook/style-isolation.md
Style isolation is one of the most critical aspects of micro-frontend architecture. When multiple applications run in the same browser context, CSS conflicts can cause visual inconsistencies and broken layouts. This guide covers various strategies to achieve effective style isolation in qiankun applications.
When multiple micro applications are loaded into the same page, they share the same global CSS namespace. This can lead to:
/* Main Application */
.button {
background: blue;
padding: 10px;
}
/* Micro Application */
.button {
background: red; /* This will override main app styles! */
border: none;
}
qiankun provides several built-in style isolation mechanisms that you can enable through configuration.
The most robust isolation method using Shadow DOM:
import { start } from 'qiankun';
start({
sandbox: {
strictStyleIsolation: true
}
});
How it works:
Pros:
Cons:
A less intrusive approach using CSS scoping:
import { start } from 'qiankun';
start({
sandbox: {
experimentalStyleIsolation: true
}
});
How it works:
Pros:
Cons:
CSS-in-JS libraries provide natural style isolation by generating unique class names.
// Micro Application using Styled Components
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'grey'};
padding: 10px 20px;
border: none;
border-radius: 4px;
&:hover {
opacity: 0.8;
}
`;
function MyComponent() {
return (
<div>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = css`
background: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
&:hover {
background: darkblue;
}
`;
function MyComponent() {
return <button css={buttonStyle}>Click me</button>;
}
/* Button.module.css */
.button {
background: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
.button:hover {
background: darkblue;
}
// Button.jsx
import styles from './Button.module.css';
function Button({ children, ...props }) {
return (
<button className={styles.button} {...props}>
{children}
</button>
);
}
Use Block, Element, Modifier naming convention to avoid conflicts:
/* Main Application */
.main-app__button {
background: blue;
}
.main-app__button--primary {
background: darkblue;
}
/* Micro Application */
.micro-app__button {
background: red;
}
.micro-app__button--large {
padding: 15px 30px;
}
Add unique prefixes to all CSS classes:
/* User Management Micro App */
.user-mgmt-container { }
.user-mgmt-header { }
.user-mgmt-button { }
/* Product Catalog Micro App */
.product-cat-container { }
.product-cat-header { }
.product-cat-button { }
Use CSS variables for consistent theming without conflicts:
/* Main Application - Define theme variables */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
}
/* Micro Application - Use theme variables */
.micro-app-button {
background: var(--primary-color);
color: white;
}
Load CSS dynamically when micro applications mount:
// Lifecycle hooks for dynamic CSS loading
const lifeCycles = {
async beforeMount(app) {
// Load micro app specific CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `${app.entry}/static/css/main.css`;
link.id = `${app.name}-styles`;
document.head.appendChild(link);
},
async afterUnmount(app) {
// Remove micro app CSS
const link = document.getElementById(`${app.name}-styles`);
if (link) {
document.head.removeChild(link);
}
}
};
registerMicroApps([
{
name: 'micro-app',
entry: '//localhost:8080',
container: '#container',
activeRule: '/micro-app'
}
], lifeCycles);
Use PostCSS plugins to automatically scope CSS:
// postcss.config.js
module.exports = {
plugins: [
require('postcss-prefixwrap')('.micro-app-container')
]
};
Input CSS:
.button {
background: blue;
}
Output CSS:
.micro-app-container .button {
background: blue;
}
// Button.module.css
.button {
background: blue;
color: white;
}
// Button.jsx
import styles from './Button.module.css';
function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}
import { useMemo } from 'react';
function useCSSPrefix(prefix) {
return useMemo(() => {
return (className) => `${prefix}-${className}`;
}, [prefix]);
}
// Usage
function MyComponent() {
const cx = useCSSPrefix('user-mgmt');
return (
<div className={cx('container')}>
<button className={cx('button')}>Click me</button>
</div>
);
}
<template>
<div class="component">
<button class="button">Click me</button>
</div>
</template>
<style scoped>
.component {
padding: 20px;
}
.button {
background: blue;
color: white;
}
</style>
<template>
<div :class="$style.component">
<button :class="$style.button">Click me</button>
</div>
</template>
<style module>
.component {
padding: 20px;
}
.button {
background: blue;
color: white;
}
</style>
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `
<div class="component">
<button class="button">Click me</button>
</div>
`,
styles: [`
.component {
padding: 20px;
}
.button {
background: blue;
color: white;
}
`],
encapsulation: ViewEncapsulation.Emulated // Default
})
export class MyComponent { }
Create a shared design system that all micro applications consume:
// shared-design-system/Button.js
export const Button = ({ variant = 'primary', size = 'medium', children, ...props }) => {
const baseClasses = 'btn';
const variantClasses = {
primary: 'btn-primary',
secondary: 'btn-secondary'
};
const sizeClasses = {
small: 'btn-sm',
medium: 'btn-md',
large: 'btn-lg'
};
const className = [
baseClasses,
variantClasses[variant],
sizeClasses[size]
].join(' ');
return <button className={className} {...props}>{children}</button>;
};
/* Design system CSS */
:root {
--btn-primary-bg: #007bff;
--btn-primary-color: white;
--btn-secondary-bg: #6c757d;
--btn-secondary-color: white;
--btn-padding-sm: 8px 12px;
--btn-padding-md: 10px 16px;
--btn-padding-lg: 12px 20px;
}
.btn {
border: none;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
}
.btn-primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-color);
}
.btn-secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-color);
}
.btn-sm { padding: var(--btn-padding-sm); }
.btn-md { padding: var(--btn-padding-md); }
.btn-lg { padding: var(--btn-padding-lg); }
When using third-party libraries that inject global styles:
// Load third-party CSS in isolation
const loadLibraryStyles = (libraryName, cssUrl) => {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const iframeDoc = iframe.contentDocument;
const link = iframeDoc.createElement('link');
link.rel = 'stylesheet';
link.href = cssUrl;
link.onload = () => {
// Extract and scope the CSS
const styles = Array.from(iframeDoc.styleSheets[0].cssRules)
.map(rule => rule.cssText)
.join('\n');
const scopedStyles = scopeCSS(styles, `.${libraryName}-container`);
const style = document.createElement('style');
style.textContent = scopedStyles;
style.id = `${libraryName}-scoped-styles`;
document.head.appendChild(style);
document.body.removeChild(iframe);
resolve();
};
iframeDoc.head.appendChild(link);
});
};
import { createGlobalStyle } from 'styled-components';
// Scope third-party styles to specific container
const AntdStyles = createGlobalStyle`
.micro-app-container {
.ant-btn {
/* Override Ant Design button styles specifically for this micro app */
border-radius: 8px;
}
.ant-table {
/* Override Ant Design table styles */
border: 1px solid #f0f0f0;
}
}
`;
function MicroApp() {
return (
<div className="micro-app-container">
<AntdStyles />
</div>
);
}
// Add to browser console for debugging
const findStyleConflicts = (selector) => {
const elements = document.querySelectorAll(selector);
elements.forEach((el, index) => {
const styles = window.getComputedStyle(el);
const rules = [];
for (let i = 0; i < document.styleSheets.length; i++) {
try {
const sheet = document.styleSheets[i];
const cssRules = sheet.cssRules || sheet.rules;
for (let j = 0; j < cssRules.length; j++) {
if (el.matches(cssRules[j].selectorText)) {
rules.push({
selector: cssRules[j].selectorText,
rule: cssRules[j].cssText,
sheet: sheet.href || 'inline'
});
}
}
} catch (e) {
// Cross-origin stylesheet
}
}
console.log(`Element ${index + 1}:`, el);
console.log('Applied styles:', rules);
});
};
// Usage: findStyleConflicts('.button');
// Track which micro app loaded which styles
const styleTracker = {
styles: new Map(),
track(appName, styleElement) {
if (!this.styles.has(appName)) {
this.styles.set(appName, []);
}
this.styles.get(appName).push(styleElement);
},
getByApp(appName) {
return this.styles.get(appName) || [];
},
getConflicts() {
const allSelectors = new Map();
this.styles.forEach((styles, appName) => {
styles.forEach(style => {
// Parse CSS and check for selector conflicts
// Implementation depends on CSS parser
});
});
return allSelectors;
}
};
// Detect style conflicts at runtime
const detectStyleConflicts = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.tagName === 'STYLE' || node.tagName === 'LINK') {
checkForConflicts(node);
}
});
}
});
});
observer.observe(document.head, { childList: true });
};
const checkForConflicts = (styleNode) => {
// Implementation to check for CSS selector conflicts
console.warn('New style node added:', styleNode);
};
// Optimize CSS loading for micro applications
const optimizedCSSLoader = {
cache: new Map(),
async loadCSS(url, appName) {
if (this.cache.has(url)) {
return this.cache.get(url);
}
const response = await fetch(url);
const css = await response.text();
const optimizedCSS = this.optimizeCSS(css, appName);
this.cache.set(url, optimizedCSS);
return optimizedCSS;
},
optimizeCSS(css, appName) {
// Remove unused selectors
// Add app-specific prefixes
// Minify if needed
return css.replace(/(\.[a-zA-Z-_]+)/g, `.${appName}-$1`);
}
};
// webpack.config.js - Split CSS by micro app
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
}
};
Here's a complete example of a well-isolated micro application:
// MicroApp.jsx
import React from 'react';
import styled, { createGlobalStyle } from 'styled-components';
// Global styles scoped to this micro app
const GlobalStyles = createGlobalStyle`
.user-management-app {
font-family: 'Inter', sans-serif;
* {
box-sizing: border-box;
}
}
`;
// Styled components with isolation
const Container = styled.div`
padding: 20px;
background: #f8f9fa;
min-height: 100vh;
`;
const Header = styled.h1`
color: #343a40;
margin-bottom: 20px;
`;
const Button = styled.button`
background: ${props => props.primary ? '#007bff' : '#6c757d'};
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.9;
}
`;
function UserManagementApp() {
return (
<div className="user-management-app">
<GlobalStyles />
<Container>
<Header>User Management</Header>
<Button primary>Add User</Button>
<Button>Cancel</Button>
</Container>
</div>
);
}
export default UserManagementApp;