The question: "Can we build microfrontends using only vanilla JavaScript, HTML, and CSS (without frameworks like React/Vue or build tools like Webpack)?"
Short answer: Yes — absolutely!
We can build microfrontends with plain HTML/JS/CSS using simple, browser-native patterns: Web Components (Custom Elements + Shadow DOM), dynamic script/ES module loading, or iframes.
Below, I'll provide the complete, copy-pasteable approach for a host and two remotes using custom elements and a tiny global event bus. This solution is minimal, framework-free, and easy to reason about. Perfect as a short engineering exercise or a workshop: lightweight, independent deployables, and easy to reason about.
Architecture overview
- Host: Loads and orchestrates microfrontends (iframes)
- Products: Microfrontend that emits cart:add events
- Cart: Microfrontend that receives events and displays cart items
📁 Directory structure
host/
index.html
products/
products.html
cart/
cart.html
Run each folder on a different local server:
Host → http://localhost:3000
Products → http://localhost:3001
Cart → http://localhost:3002
(You can use python -m http.server or VSCode Live Server.)
🖥️ Host app — host/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Host (iframes)</title>
<style>
nav{background:#222;color:#fff;padding:8px}
#frameWrap{display:flex;gap:12px;padding:12px}
iframe{width:48%;height:400px;border:1px solid #ccc;}
</style>
</head>
<body>
<nav>
<button id="btnLoad">Load Microfrontends</button>
<button id="btnSendAuth">Send Auth Token</button>
</nav>
<div id="frameWrap"></div>
<script>
const ALLOWED = {
products: 'http://localhost:3001',
cart: 'http://localhost:3002'
};
const frameWrap = document.getElementById('frameWrap');
function createIframe(id, src, sandbox = 'allow-scripts allow-same-origin') {
const f = document.createElement('iframe');
f.id = id;
f.src = src;
f.sandbox = sandbox;
f.loading = 'lazy';
f.title = id;
frameWrap.appendChild(f);
return f;
}
// Handle messages from remotes
window.addEventListener('message', (ev) => {
const origin = ev.origin;
const data = ev.data || {};
if (!data.type) return;
if (![ALLOWED.products, ALLOWED.cart].includes(origin)) {
console.warn('Blocked message from unknown origin:', origin);
return;
}
console.log('Host received:', data, 'from', origin);
// Forward "cart:add" to Cart MFE
if (data.type === 'cart:add') {
const cartFrame = document.getElementById('cartFrame');
if (cartFrame && cartFrame.contentWindow) {
cartFrame.contentWindow.postMessage(data, ALLOWED.cart);
}
}
// Handle auth requests
if (data.type === 'auth:request') {
const token = 'FAKE_TOKEN_12345';
ev.source.postMessage({ type: 'auth:response', payload: { token } }, origin);
}
// Dynamic iframe resizing
if (data.type === 'frame:resize' && ev.source === document.getElementById('productsFrame')?.contentWindow) {
document.getElementById('productsFrame').style.height = data.payload + 'px';
}
});
document.getElementById('btnLoad').onclick = () => {
frameWrap.innerHTML = '';
createIframe('productsFrame', ALLOWED.products + '/products.html');
createIframe('cartFrame', ALLOWED.cart + '/cart.html');
};
document.getElementById('btnSendAuth').onclick = () => {
const token = 'FAKE_TOKEN_12345';
const prod = document.getElementById('productsFrame');
const cart = document.getElementById('cartFrame');
if (prod && prod.contentWindow) {
prod.contentWindow.postMessage({ type:'auth:response', payload:{ token } }, ALLOWED.products);
}
if (cart && cart.contentWindow) {
cart.contentWindow.postMessage({ type:'auth:response', payload:{ token } }, ALLOWED.cart);
}
};
</script>
</body>
</html>
📦 Products microfrontend — products/products.html
<!doctype html>
<html>
<head><meta charset="utf-8"/><title>Products</title></head>
<body>
<h3>Products (iframe)</h3>
<div id="list"></div>
<script>
const ORIGIN_PARENT = 'http://localhost:3000';
const products = [
{id:1,name:'Widget A',price:199},
{id:2,name:'Widget B',price:299}
];
function render(){
document.getElementById('list').innerHTML = products.map(p => `
<div style="display:flex;justify-content:space-between;padding:6px;border-bottom:1px solid #eee">
<div>${p.name} — ₹${p.price}</div>
<button data-id="${p.id}">Add</button>
</div>`).join('');
document.querySelectorAll("button").forEach(btn => {
btn.onclick = () => {
const id = +btn.dataset.id;
const item = products.find(x => x.id === id);
parent.postMessage({ type:'cart:add', payload:item }, ORIGIN_PARENT);
};
});
}
render();
// Request auth
parent.postMessage({ type:'auth:request' }, ORIGIN_PARENT);
// Listen for messages
window.addEventListener('message', (ev) => {
if (ev.origin !== ORIGIN_PARENT) return;
if (ev.data.type === 'auth:response') {
console.log('Products received token:', ev.data.payload.token);
}
});
// Ask host to adjust height
setTimeout(() => {
parent.postMessage({ type:'frame:resize', payload: document.body.scrollHeight }, ORIGIN_PARENT);
}, 200);
</script>
</body>
</html>
🛒 Cart microfrontend — cart/cart.html
<!doctype html>
<html>
<head><meta charset="utf-8"/><title>Cart</title></head>
<body>
<h3>Cart (iframe)</h3>
<div id="items">No items</div>
<script>
const ORIGIN_PARENT = 'http://localhost:3000';
const items = [];
function render(){
document.getElementById('items').innerHTML = items.length
? items.map(it => `<div>${it.name} — ₹${it.price}</div>`).join('')
: 'No items';
}
render();
window.addEventListener('message', (ev) => {
if (ev.origin !== ORIGIN_PARENT) return;
const msg = ev.data;
if (msg.type === 'cart:add') {
items.push(msg.payload);
render();
}
if (msg.type === 'auth:response') {
console.log('Cart received token:', msg.payload.token);
}
});
// Request token at startup
parent.postMessage({ type:'auth:request' }, ORIGIN_PARENT);
</script>
</body>
</html>
How to test locally
cd host && python -m http.server 3000
cd products && python -m http.server 3001
cd cart && python -m http.server 3002
- Visit: http://localhost:3000
- Action: Click “Load Microfrontends”
- Action: Click “Add” in Products — items should appear in Cart
- Observe: Browser console logs for auth:request, auth:response, cart:add, etc.
🔐 Security and production notes
- Origin checks: Verify ev.origin for every message handler. Never trust postMessage() without origin checks.
- Iframe sandboxing: Use sandbox and grant minimal permissions (allow-scripts, allow-same-origin only if needed).
- Token handling: Prefer short-lived tokens via postMessage (not URLs).
- Transport: Serve over HTTPS in production.
- CSP: Apply strict Content Security Policy headers on remote pages.
✅ Acceptance criteria
- Host orchestration: Host loads two independent microfrontends in iframes.
- Event flow: Products iframe sends cart:add events; host forwards them to Cart iframe.
- UI: Cart displays added items correctly.
- Security: Messages are origin-checked and tokens are exchanged via postMessage.
- Isolation: Iframes can be swapped/updated independently.
Jai Chinjo
Top comments (0)