Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Parent sends a message to an iframe
const iframeRef = useRef<HTMLIFrameElement>(null);
function sendToIframe(data: unknown) {
iframeRef.current?.contentWindow?.postMessage(
data,
"https://widget.example.com" // ALWAYS specify the target origin
);
}
// Iframe (or any window) listens for messages
useEffect(() => {
function handleMessage(event: MessageEvent) {
// CRITICAL: always validate the origin
if (event.origin !== "https://parent.example.com") return;
console.log("Received:", event.data);
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);When to reach for this: When you need communication between a parent page and an embedded iframe, between windows opened with window.open, or between any two browsing contexts that cannot share JavaScript scope directly. Common use cases include micro-frontends, embedded third-party widgets, payment forms, OAuth popups, and cross-origin auth flows.
A parent page sends a theme object to an embedded iframe. The iframe applies the theme and sends back an acknowledgement.
import { useEffect, useRef, useState } from "react";
// -- Typed message protocol using discriminated unions --
type ParentMessage =
| { type: "THEME_UPDATE"; payload: Theme }
| { type: "PING" };
type IframeMessage =
| { type: "THEME_ACK"; payload: { appliedAt: number } }
| { type: "PONG" };
interface Theme {
mode: "light" | "dark";
primaryColor: string;
fontSize: number;
}
const IFRAME_ORIGIN = "https://widget.example.com";
function ParentApp() {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [acked, setAcked] = useState(false);
// Listen for responses from the iframe
useEffect(() => {
function handleMessage(event: MessageEvent<IframeMessage>) {
if (event.origin !== IFRAME_ORIGIN) return;
switch (event.data.type) {
case "THEME_ACK":
console.log("Theme applied at:", event.data.payload.appliedAt);
setAcked(true);
break;
case "PONG":
console.log("Iframe is alive");
break;
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
function sendTheme(mode: "light" | "dark") {
const message: ParentMessage = {
type: "THEME_UPDATE",
payload: { mode, primaryColor: "#3b82f6", fontSize: 16 },
};
iframeRef.current?.contentWindow?.postMessage(message, IFRAME_ORIGIN);
setAcked(false);
}
return (
<div>
<h1>Parent App</h1>
<button onClick={() => sendTheme("light")}>Light Theme</button>
<button onClick={() => sendTheme("dark")}>Dark Theme</button>
{acked && <p>Theme acknowledged by widget</p>}
<iframe
ref={iframeRef}
src={`${IFRAME_ORIGIN}/widget`}
title="Embedded Widget"
style={{ width: 600, height: 400, border: "1px solid #ccc" }}
/>
</div>
);
}import { useEffect, useState } from "react";
interface Theme {
mode: "light" | "dark";
primaryColor: string;
fontSize: number;
}
type ParentMessage =
| { type: "THEME_UPDATE"; payload: Theme }
| { type: "PING" };
type IframeMessage =
| { type: "THEME_ACK"; payload: { appliedAt: number } }
| { type: "PONG" };
const PARENT_ORIGIN = "https://parent.example.com";
function Widget() {
const [theme, setTheme] = useState<Theme>({
mode: "light",
primaryColor: "#3b82f6",
fontSize: 16,
});
useEffect(() => {
function handleMessage(event: MessageEvent<ParentMessage>) {
// CRITICAL: validate origin before processing
if (event.origin !== PARENT_ORIGIN) return;
switch (event.data.type) {
case "THEME_UPDATE":
setTheme(event.data.payload);
// Send acknowledgement back to parent
const ack: IframeMessage = {
type: "THEME_ACK",
payload: { appliedAt: Date.now() },
};
// event.source is the window that sent the message
(event.source as Window).postMessage(ack, event.origin);
break;
case "PING":
(event.source as Window).postMessage(
{ type: "PONG" } satisfies IframeMessage,
event.origin
);
break;
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
return (
<div
style={{
backgroundColor: theme.mode === "dark" ? "#1e293b" : "#ffffff",
color: theme.mode === "dark" ? "#f1f5f9" : "#0f172a",
fontSize: theme.fontSize,
padding: 24,
minHeight: "100vh",
}}
>
<h2 style={{ color: theme.primaryColor }}>Widget Content</h2>
<p>Current theme: {theme.mode}</p>
</div>
);
}window.postMessage() is the browser-standard API for safe cross-origin communication between browsing contexts (windows, iframes, popups, workers).targetWindow.postMessage(message, targetOrigin, transfer?).Every message handler receives a MessageEvent with these key properties:
| Property | Type | Description |
|---|---|---|
data | any | The message payload (structured-clone of what was sent) |
origin | string | The origin of the sending window (e.g., https://example.com) |
source | Window or MessagePort or ServiceWorker or null | Reference to the sender window; use to reply |
ports | ReadonlyArray<MessagePort> | MessageChannel ports transferred with the message |
lastEventId | string | Empty string for postMessage (used by SSE) |
postMessage(data, targetOrigin) on the target window object (not its own).targetOrigin. If it does not match, the message is silently dropped.Date, Map, Set, ArrayBuffer, Blob, File, RegExp, typed arrays, and nested objects. It does not support functions, DOM nodes, Symbol, or WeakMap/WeakSet."message" event listener fires with a MessageEvent containing the cloned data, the sender's origin, and a reference to the sender's window.event.origin in every message handler. Without this check, any page that can iframe your page (or that your page iframes) can send arbitrary messages to your handler.targetOrigin when calling postMessage. Using "*" means any origin can receive the message. This is dangerous when the message contains sensitive data (tokens, user info, etc.). See the security doc for full details.// Clean useEffect pattern for postMessage listeners
useEffect(() => {
const ALLOWED_ORIGINS = new Set([
"https://widget-a.example.com",
"https://widget-b.example.com",
]);
function handleMessage(event: MessageEvent) {
if (!ALLOWED_ORIGINS.has(event.origin)) return;
// Type-narrow based on discriminated union
const msg = event.data;
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
switch (msg.type) {
case "THEME_UPDATE":
// handle...
break;
// ...
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []); // Empty deps: listener is stable, no stale closures needed// Define all messages in a union. The "type" field is the discriminant.
type AppMessage =
| { type: "NAVIGATE"; payload: { path: string } }
| { type: "AUTH_TOKEN"; payload: { token: string; expiresAt: number } }
| { type: "RESIZE"; payload: { width: number; height: number } }
| { type: "READY" }; // no payload needed
// Type guard to validate unknown incoming data
function isAppMessage(data: unknown): data is AppMessage {
if (typeof data !== "object" || data === null) return false;
if (!("type" in data)) return false;
const d = data as { type: string };
return ["NAVIGATE", "AUTH_TOKEN", "RESIZE", "READY"].includes(d.type);
}
// Usage in handler
function handleMessage(event: MessageEvent) {
if (event.origin !== EXPECTED_ORIGIN) return;
if (!isAppMessage(event.data)) return;
// TypeScript now narrows correctly in each case
switch (event.data.type) {
case "AUTH_TOKEN":
// event.data.payload is { token: string; expiresAt: number }
setToken(event.data.payload.token);
break;
case "NAVIGATE":
// event.data.payload is { path: string }
router.push(event.data.payload.path);
break;
}
}event.origin validation, even in development. Build the habit early."*" as target origin broadcasts to any origin. Only use it for truly public, non-sensitive messages (and even then, think twice).postMessage does not return a value. If you need a response, you must implement a request/response protocol with message IDs (covered in the intermediate guide).event.source can be null if the sending window has been closed before your handler fires. Always null-check before replying.contentWindow before the iframe's load event fires will silently fail. Use the iframe's onLoad callback or wait for a READY message from the iframe.type field to route to the correct handler.Date objects, Map, Set, etc., but it will throw on functions and DOM nodes. If you need to verify what is cloneable, use structuredClone() locally to test.| Approach | When to Use |
|---|---|
| BroadcastChannel | Same-origin tabs or windows that need to share state (no cross-origin support) |
| Channel Messaging (MessageChannel) | Dedicated two-way port between exactly two contexts (covered in advanced guide) |
| SharedWorker | Multiple same-origin tabs sharing a single worker for state coordination |
| Custom Events | Communication within the same document (no cross-origin, no cross-frame) |
| Server-Sent Events or WebSocket | When you need server-mediated communication between clients |
| URL fragment or query params | One-time data passing to an iframe (no ongoing communication) |
Date, Map, Set, ArrayBuffer, Blob, File, and RegExp.Symbol, WeakMap, and WeakSet are not supported and will throw a DataCloneError.Set of known-good origins for fast, exact-match validation."*" for truly public, non-sensitive messages.type AppMessage =
| { type: "NAVIGATE"; payload: { path: string } }
| { type: "READY" };
function handleMessage(event: MessageEvent<AppMessage>) {
if (event.origin !== EXPECTED_ORIGIN) return;
switch (event.data.type) {
case "NAVIGATE":
// event.data.payload is { path: string }
break;
}
}function isAppMessage(data: unknown): data is AppMessage {
if (typeof data !== "object" || data === null) return false;
if (!("type" in data)) return false;
const d = data as { type: string };
return ["NAVIGATE", "READY"].includes(d.type);
}contentWindow is not ready.onLoad callback or wait for a READY message from the iframe before sending.postMessage does not return a value. If you need a response, you must build a request/response protocol with correlation IDs.Set lookup is an exact string match, which is both safer and faster.Date, Map, Set, ArrayBuffer, Blob, circular references, and more.Date becomes a string, Map becomes {}).JSON.stringify before sending or JSON.parse on receive.event.source is null.event.source before calling postMessage on it to reply.useEffect(() => {
function handleMessage(event: MessageEvent) {
if (event.origin !== EXPECTED) return;
// handle message
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);How to Send Data From iframe To Parent Page - JavaScript postMessage Tutorial
Window postMessage() protocol using React - #1 JavaScript Tutorials
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥