All guides

Talk to an iframe with postMessage

Cross-origin pages cannot call each other's code, but they can send messages with window.postMessage. The sender names the origin that may receive the message. The receiver checks event.origin before it uses the data.

5 min read · Updated

The two sides

postMessage is the safe channel between two windows with different origins. In these examples:

  • The parent page is https://partner.example. It holds the iframe.
  • The framed page is https://your-site.com. It loads inside the iframe.

Each side sends with postMessage and receives with a message event listener.

Send a message

From the parent to the frame

const frame = document.getElementById("widget");

frame.contentWindow.postMessage({ type: "theme", value: "dark" }, "https://your-site.com");

The second argument is the target origin. The browser delivers the message only if the frame still shows a page from that origin. If the frame went to another site, the browser drops the message. This protects private data.

From the frame to the parent

window.parent.postMessage({ type: "ready" }, "https://partner.example");

Use window.parent for the page right above the frame. Use window.top for the top page when frames are nested.

What you can send

postMessage copies the data with the structured clone algorithm. Objects, arrays, dates, Map, Set, Blob, and ArrayBuffer work. Functions and DOM nodes do not. A DataCloneError means the data has one of these.

Receive and check a message

window.addEventListener("message", (event) => {
  if (event.origin !== "https://your-site.com") return;

  const { type, value } = event.data ?? {};
  if (type === "theme") {
    document.documentElement.dataset.theme = value === "dark" ? "dark" : "light";
  }
});

Any page can send a message to any window. A bad site can open your page in a popup or a frame and post data to it. So follow these rules on the receiving side:

  1. Check event.origin first. Compare it with an exact string. Do not use includes() or startsWith(). https://your-site.com.evil.example starts with https://your-site.com.
  2. Check event.source. If the page has many frames, compare event.source with frame.contentWindow.
  3. Treat event.data as untrusted input. Check its type and shape. Never pass it to innerHTML, eval(), or a redirect.

If you read frame.contentWindow.document in place of a message, the browser stops the script with SecurityError: Blocked a frame with origin. If the targetOrigin does not match the frame, the message is lost and Chrome writes the target origin does not match the recipient window.

The target origin "*"

"*" means "any origin". Use it only for data that is safe to show to any site, such as a height. Never send tokens, user data, or form values with "*".

Wait until the frame is ready

A message sent before the frame loads is lost. The load event of the iframe is not enough, because the app inside may start later. Let the frame say when it is ready:

// In the framed page
window.addEventListener("message", (event) => {
  if (event.origin !== "https://partner.example") return;
  if (event.data?.type === "init") console.log("user:", event.data.userId);
});
window.parent.postMessage({ type: "ready" }, "https://partner.example");
// In the parent page
window.addEventListener("message", (event) => {
  if (event.origin !== "https://your-site.com") return;
  if (event.data?.type === "ready") {
    frame.contentWindow.postMessage({ type: "init", userId: "u_123" }, "https://your-site.com");
  }
});

Add the listener in the framed page before you send ready. Then no reply is lost.

Two-way requests with MessageChannel

For many requests and replies, a MessageChannel gives you a private pipe:

// Parent
const channel = new MessageChannel();
channel.port1.onmessage = (event) => console.log("reply:", event.data);
frame.contentWindow.postMessage({ type: "connect" }, "https://your-site.com", [channel.port2]);
// Framed page
window.addEventListener("message", (event) => {
  if (event.origin !== "https://partner.example") return;
  if (event.data?.type !== "connect") return;
  const port = event.ports[0];
  port.onmessage = (msg) => port.postMessage({ ok: true, echo: msg.data });
});

After the first message, the two pages talk on the port. No other window can read or send on it.

Check that the frame loads

postMessage cannot help if the frame never loads. Paste the framed URL into the iframe tester to see if its server allows framing.

Sources

Check your page now

Know before you embed.

Free, no sign-up, nothing stored. Paste a URL and get a verdict, a live preview, and the fix.