How to communicate between iframe and the page?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#JavaScript #Browser #Security

Brief Answer

Two approaches:

  • Same origin: direct access (parent, top, iframe.contentWindow).
  • Cross origin: window.postMessage + message event handler.

Short examples:

// Parent → iframe (cross-origin)
iframeEl.contentWindow.postMessage({ type: 'PING' }, 'https://child.example');
// iframe → parent
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://parent.example') return;
  if (e.data?.type === 'PING') e.source?.postMessage({ type: 'PONG' }, e.origin);
});
// Same-origin: from iframe
parent.doSomething?.();

— Always validate event.origin and use exact targetOrigin (not *).

Short:

// storage event (same-origin)
window.addEventListener('storage', (e) => console.log(e.key, e.newValue));
localStorage.setItem('msg', 'hi');
// BroadcastChannel (same-origin)
const ch = new BroadcastChannel('app');
ch.onmessage = (e) => console.log(e.data);
ch.postMessage({ type: 'PING' });

Full Answer

Communication model

  • Same origin:

    • From iframe: use parent/top.
    • From parent: use iframeEl.contentWindow.
    • Prefer calling agreed functions over touching DOM.
  • Cross origin:

    • Use postMessage and listen to message.
    • Set an exact targetOrigin and validate event.origin.
// Parent → iframe
iframeEl.addEventListener('load', () => {
  iframeEl.contentWindow.postMessage({ type: 'INIT' }, 'https://child.example');
});
// iframe: receive
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://parent.example') return;
  if (e.data?.type === 'INIT') e.source?.postMessage({ ok: true }, e.origin);
});

Security and reliability

  • Validate event.origin, data type, and message schema.
  • Avoid targetOrigin: "*", prefer exact origin.
  • Attach and cleanup listeners on component lifecycle.
  • Define a simple protocol: type, payload, requestId.
<!-- iframe restrictions -->
<iframe src="https://child.example"
        sandbox="allow-scripts allow-same-origin"
        referrerpolicy="no-referrer"></iframe>

Optional: MessageChannel

Bidirectional channel with ports:

const { port1, port2 } = new MessageChannel();
iframeEl.contentWindow.postMessage({ type: 'CHANNEL' }, 'https://child.example', [port2]);
port1.onmessage = (e) => { /* handle */ };

Common pitfalls

  • Posting before iframe load.
  • No origin validation or using *.
  • Large payloads and complex objects (keep it compact).
  • Attempting cross-origin DOM access (not allowed; use postMessage).

Summary

  • Same origin → direct function calls between windows.
  • Cross origin → postMessage with strict origin checks and a clear protocol.

Additional channels (same-origin)

  • window.parent/window.frames:
// Direct calls
parent.doSomething?.(); // from&nbsp;iframe
iframeEl.contentWindow.doTask?.(); // from&nbsp;parent
  • localStorage + storage:
// Send
localStorage.setItem('command', 'REFRESH');
// Receive in&nbsp;another window/iframe of&nbsp;same origin
window.addEventListener('storage', (e) => { if (e.key === 'command') {/* ... */} });
  • BroadcastChannel:
const channel = new BroadcastChannel('ui-sync');
channel.onmessage = (e) => {/* ... */};
channel.postMessage({ type: 'READY' });

Tabs and popups

  • Use BroadcastChannel or localStorage+storage.
  • Cross-origin still requires postMessage.

Security notes

  • Validate event.origin, type/payload.
  • Use exact targetOrigin (not *).
  • Restrict iframe via sandbox.