Iframe Composition
iFrame-based micro-frontend composition for maximum isolation between independently deployed frontend applications.
You are an expert in iFrame-based composition for building micro-frontend architectures. You help teams achieve the strongest possible isolation between independently deployed frontend applications while maintaining a cohesive user experience through structured inter-frame communication. ## Key Points - Integrating third-party applications you do not control. - Embedding legacy applications that cannot be refactored. - Scenarios where absolute fault isolation is non-negotiable (e.g., financial dashboards embedding untrusted widgets). - DOM tree and CSSOM (complete style isolation). - JavaScript global scope (`window`, `document`). - Navigation history entry. - Cookie jar (subject to `SameSite` and third-party cookie policies). 1. **`postMessage` / `MessageChannel`** — the primary API for cross-origin or same-origin frame communication. 2. **URL fragment / query parameters** — pass initial configuration when loading the iFrame. 3. **Shared storage** — `localStorage`, `sessionStorage`, or `BroadcastChannel` (same-origin only). 1. **Always validate `event.origin`** — never use `"*"` as the origin check in production `message` handlers. This is a cross-site scripting vector. 2. **Namespace messages** — add a `namespace` field to every message to avoid collisions with third-party scripts that also use `postMessage`.
skilldb get micro-frontend-skills/Iframe CompositionFull skill: 247 linesiFrame Composition — Micro-Frontends
You are an expert in iFrame-based composition for building micro-frontend architectures. You help teams achieve the strongest possible isolation between independently deployed frontend applications while maintaining a cohesive user experience through structured inter-frame communication.
Overview
iFrames are the oldest and most battle-tested approach to embedding one web application inside another. Each micro-frontend runs in its own browsing context with a fully isolated DOM, CSS scope, and JavaScript global. This guarantees zero style collisions and zero script interference. The tradeoff is added complexity around communication, performance, accessibility, and responsive layout.
iFrame composition is best suited for:
- Integrating third-party applications you do not control.
- Embedding legacy applications that cannot be refactored.
- Scenarios where absolute fault isolation is non-negotiable (e.g., financial dashboards embedding untrusted widgets).
Core Concepts
Browsing Context Isolation
Each iFrame creates a separate browsing context with its own:
- DOM tree and CSSOM (complete style isolation).
- JavaScript global scope (
window,document). - Navigation history entry.
- Cookie jar (subject to
SameSiteand third-party cookie policies).
Communication Channels
Since iFrames are isolated by the same-origin policy (or the sandbox attribute), communication happens via:
postMessage/MessageChannel— the primary API for cross-origin or same-origin frame communication.- URL fragment / query parameters — pass initial configuration when loading the iFrame.
- Shared storage —
localStorage,sessionStorage, orBroadcastChannel(same-origin only).
Sandbox Attribute
The sandbox attribute restricts iFrame capabilities. Permissions are opt-in:
| Token | Grants |
|---|---|
allow-scripts | JavaScript execution |
allow-same-origin | Retain origin (needed for cookies, storage) |
allow-forms | Form submission |
allow-popups | window.open and target="_blank" |
allow-modals | alert(), confirm(), prompt() |
Implementation Patterns
Basic Shell with iFrame Slots
<!-- shell/index.html -->
<!DOCTYPE html>
<html>
<head>
<style>
.mf-frame {
width: 100%;
border: none;
display: block;
}
.layout {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}
</style>
</head>
<body>
<div class="layout">
<nav id="shell-nav"><!-- shell-owned navigation --></nav>
<iframe
id="content-frame"
class="mf-frame"
src="https://catalog.example.com"
sandbox="allow-scripts allow-same-origin allow-forms"
loading="lazy"
></iframe>
</div>
</body>
</html>
Structured postMessage Protocol
// shared/message-bus.js — used by both shell and micro-frontends
const MESSAGE_NAMESPACE = "mf-bus";
export function send(target, type, payload) {
target.postMessage(
{ namespace: MESSAGE_NAMESPACE, type, payload, timestamp: Date.now() },
"*" // In production, always specify the target origin
);
}
export function listen(expectedOrigin, handlers) {
function onMessage(event) {
if (expectedOrigin !== "*" && event.origin !== expectedOrigin) return;
const { namespace, type, payload } = event.data || {};
if (namespace !== MESSAGE_NAMESPACE) return;
if (handlers[type]) {
handlers[type](payload, event);
}
}
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}
Shell Sending Navigation Events to iFrame
// shell/router.js
import { send } from "./message-bus.js";
const frame = document.getElementById("content-frame");
function navigateTo(path) {
// Option A: Change iframe src (full reload)
// frame.src = `https://catalog.example.com${path}`;
// Option B: Send a message so the iframe can handle it with client-side routing
send(frame.contentWindow, "NAVIGATE", { path });
}
document.querySelector("#shell-nav").addEventListener("click", (e) => {
if (e.target.dataset.route) {
navigateTo(e.target.dataset.route);
}
});
Micro-Frontend Responding to Shell Messages
// catalog/src/shell-bridge.js
import { listen, send } from "@myorg/message-bus";
const cleanup = listen("https://shell.example.com", {
NAVIGATE({ path }) {
// Use the app's internal router
window.history.pushState(null, "", path);
window.dispatchEvent(new PopStateEvent("popstate"));
},
THEME_CHANGE({ theme }) {
document.documentElement.setAttribute("data-theme", theme);
},
});
// Notify the shell that the micro-frontend is ready
send(window.parent, "MF_READY", { name: "catalog" });
Dynamic iFrame Resizing
// Inside the iFrame — report height changes to the shell
const resizeObserver = new ResizeObserver(([entry]) => {
const height = entry.borderBoxSize?.[0]?.blockSize || entry.target.scrollHeight;
window.parent.postMessage(
{ namespace: "mf-bus", type: "RESIZE", payload: { height } },
"https://shell.example.com"
);
});
resizeObserver.observe(document.body);
// Shell — adjust iframe height
listen("https://catalog.example.com", {
RESIZE({ height }) {
document.getElementById("content-frame").style.height = `${height}px`;
},
});
MessageChannel for Point-to-Point Communication
// Shell creates a MessageChannel and passes one port to the iFrame
const channel = new MessageChannel();
// Keep port1 in the shell
channel.port1.onmessage = (event) => {
console.log("From child:", event.data);
};
// Transfer port2 to the iFrame
const frame = document.getElementById("content-frame");
frame.addEventListener("load", () => {
frame.contentWindow.postMessage(
{ namespace: "mf-bus", type: "INIT_CHANNEL" },
"https://catalog.example.com",
[channel.port2] // transferred, not cloned
);
});
// Inside the iFrame — receive the port
window.addEventListener("message", (event) => {
if (event.data?.type === "INIT_CHANNEL" && event.ports.length) {
const port = event.ports[0];
port.onmessage = (msg) => console.log("From shell:", msg.data);
port.postMessage({ status: "connected" });
}
});
Best Practices
- Always validate
event.origin— never use"*"as the origin check in productionmessagehandlers. This is a cross-site scripting vector. - Namespace messages — add a
namespacefield to every message to avoid collisions with third-party scripts that also usepostMessage. - Use
loading="lazy"— defer offscreen iFrame loads to improve initial page performance. - Minimize iFrame count — each iFrame is a full browsing context. Two or three is fine; ten will hurt memory and CPU.
- Implement a ready handshake — the shell should wait for an
MF_READYmessage before sending commands; the iFrame might not have its listener registered yet. - Coordinate focus management — keyboard focus does not naturally flow between the shell and iFrame. Manage focus transitions explicitly for accessibility.
- Apply
sandboxrestrictively — start with no permissions and add only what the micro-frontend needs.
Common Pitfalls
- Third-party cookie blocking — browsers increasingly block cookies in cross-origin iFrames. If the micro-frontend needs auth cookies, it must use
SameSite=None; Secureor switch to token-based auth viapostMessage. - Broken back/forward navigation — iFrame navigations push entries onto the parent's history stack, causing unexpected back-button behavior. Prefer client-side routing inside the iFrame triggered by shell messages.
- No shared layout context — the iFrame cannot participate in the parent's CSS grid or flexbox. You must explicitly synchronize sizing.
- Accessibility gaps — screen readers may announce the iFrame boundary awkwardly. Use
titleattribute on the<iframe>and ensure focus trapping does not lock users inside. - Performance overhead — each iFrame initializes its own JavaScript VM, parses its own HTML, and runs its own event loop. This is heavier than in-process composition.
- Print styling — printing a page with iFrames often excludes or clips iFrame content. Custom print handling is required.
Core Philosophy
iFrame composition provides the strongest isolation guarantees of any micro-frontend approach. Each iFrame is a fully independent browsing context with its own DOM, CSS, JavaScript global, and security origin. This makes iFrames the right choice when you need absolute fault isolation — a crash in one micro-frontend cannot affect the shell or other micro-frontends. The tradeoff is complexity in communication, layout coordination, and accessibility.
Communication between iFrames must be structured and validated. postMessage is the primary channel, and it is also a potential security vector. Always validate event.origin, namespace your messages to avoid collisions with third-party scripts, and implement a ready handshake so the shell does not send commands before the iFrame's listener is registered. Treat cross-frame communication with the same rigor you would apply to a network API.
Use iFrames selectively, not universally. Each iFrame creates a separate JavaScript VM, parses its own HTML, and runs its own event loop. This overhead is acceptable for 2-3 iFrames but degrades performance with 10+. Reserve iFrame composition for integrating third-party applications, embedding legacy systems, or scenarios where absolute isolation is non-negotiable. For in-house micro-frontends under your control, lighter composition strategies (Module Federation, Web Components) are typically more efficient.
Anti-Patterns
-
Using
"*"as the origin check in postMessage handlers — accepting messages from any origin is a cross-site scripting vector; always validateevent.originagainst the expected origin in production. -
Not namespacing postMessage messages — third-party scripts, analytics libraries, and browser extensions also use postMessage; without a namespace field, your handler may process unrelated messages.
-
Embedding more than a few iFrames per page — each iFrame initializes its own JavaScript VM and parsing pipeline; using 10+ iFrames on a single page causes noticeable memory and CPU overhead.
-
Navigating inside the iFrame with server-side navigation — iFrame navigations push entries onto the parent's history stack, causing broken back-button behavior; prefer client-side routing inside the iFrame triggered by shell messages.
-
Ignoring iFrame accessibility — screen readers may announce iFrame boundaries awkwardly; always set the
titleattribute on<iframe>elements and manage focus transitions explicitly for keyboard users.
Install this skill directly: skilldb add micro-frontend-skills
Related Skills
Deployment
Independent deployment patterns for micro-frontends including CI/CD pipelines, versioning, rollback, and environment strategies.
Design System Sharing
Strategies for sharing design systems, component libraries, and visual consistency across independently deployed micro-frontends.
Module Federation
Webpack Module Federation for sharing code and dependencies across independently deployed micro-frontends at runtime.
Routing
Cross-application routing strategies for micro-frontends including shell-controlled routing, distributed routing, and URL contracts.
Shared State
Cross-application state sharing patterns for micro-frontends including event buses, shared stores, and URL-based state.
Single Spa
Single-SPA framework for orchestrating multiple JavaScript micro-frontends within a single page application shell.