Skip to main content
Technology & EngineeringMicro Frontend247 lines

Iframe Composition

iFrame-based micro-frontend composition for maximum isolation between independently deployed frontend applications.

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

iFrame 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 SameSite and third-party cookie policies).

Communication Channels

Since iFrames are isolated by the same-origin policy (or the sandbox attribute), communication happens via:

  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 storagelocalStorage, sessionStorage, or BroadcastChannel (same-origin only).

Sandbox Attribute

The sandbox attribute restricts iFrame capabilities. Permissions are opt-in:

TokenGrants
allow-scriptsJavaScript execution
allow-same-originRetain origin (needed for cookies, storage)
allow-formsForm submission
allow-popupswindow.open and target="_blank"
allow-modalsalert(), 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

  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.
  3. Use loading="lazy" — defer offscreen iFrame loads to improve initial page performance.
  4. Minimize iFrame count — each iFrame is a full browsing context. Two or three is fine; ten will hurt memory and CPU.
  5. Implement a ready handshake — the shell should wait for an MF_READY message before sending commands; the iFrame might not have its listener registered yet.
  6. Coordinate focus management — keyboard focus does not naturally flow between the shell and iFrame. Manage focus transitions explicitly for accessibility.
  7. Apply sandbox restrictively — 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; Secure or switch to token-based auth via postMessage.
  • 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 title attribute 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 validate event.origin against 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 title attribute on <iframe> elements and manage focus transitions explicitly for keyboard users.

Install this skill directly: skilldb add micro-frontend-skills

Get CLI access →