The Channel Messaging API is a browser feature that lets two browsing contexts communicate directly with each other using message ports.
A “browsing context” can be: two iframes, a window and a popup, a worker and a window, or even different documents in the same tab (e.g., during navigation with portals or same-origin prerender).
Instead of broadcasting events (like postMessage to a window), it creates a dedicated, private, bi‑directional channel you can pass around.
Core concepts
MessageChannel: The object you create to establish a dedicated link. It contains two linked MessagePort instances: port1 and port2.
MessagePort: An endpoint you use to send and receive messages. Each port has:
- postMessage(data): Send any structured-clone-able data.
- onmessage / addEventListener('message', ...): Receive data.
- start(): Begin dispatching messages (required if you’re using addEventListener without onmessage).
- close(): Permanently close the port; further messages will not be delivered.
Structured cloning: The browser serializes data without JSON, preserving types like objects, arrays, Maps, Sets, ArrayBuffers, TypedArrays, Blobs, Files, and even cyclic references. Functions, DOM nodes, and certain host objects cannot be cloned.
Why use it?
Clean two-way communication: Establish a private pipe instead of scattering postMessage across windows or frames.
Isolation and composability: You can pass a port to another context and forget about it; the two ends can talk independently.
Performance: Efficient transfer of large binary data via transferable objects (e.g., ArrayBuffer), avoiding copies where possible.
Decoupling: Components (widgets, micro-frontends, embedded iframes) can communicate without tight coupling to window references.
Common use cases
Parent <-> iframe messaging: Embed a third-party widget and communicate securely without exposing global window messaging.
Window <-> popup: Coordinate auth flows or editor previews with a dedicated channel.
Window <-> Worker: While Workers have their own messaging APIs, passing a MessagePort allows multiplexing multiple logical channels or wiring third-party code that expects ports.
Cross-component bus: Build a lightweight message bus across iframes/micro-frontends, with the ability to hand off ports dynamically.
Service Worker proxying: Pass a port to a client so the SW can stream updates to a specific UI component.
How it compares to postMessage and BroadcastChannel
window.postMessage: One-off or ad-hoc messages to a known window. Simple, but not a dedicated link. Requires targetWindow references and origin checks.
BroadcastChannel: Pub/sub to all same-origin contexts. Great for fan-out (e.g., “user logged out”), but no direct pairing and no back-pressure.
Channel Messaging: One-to-one pipe with explicit endpoints and flow you control. Best for request/response patterns, streaming, or when you need multiple independent channels.
Basic pattern
- Create a channel in one context.
- Keep one port, send the other port to the peer (via postMessage or as a transfer to a Worker).
- Start listening and exchanging messages.
Channel Messaging API Demo
Main Page
Messages from iframe will appear below:
Iframe Content:
Iframe Page
Iframe Page
Messages from the main page will appear below:
/* General Body & Layout Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #f4f7f9;
color: #333;
margin: 0;
padding: 20px;
line-height: 1.6;
}
h1,
h2 {
color: #1a2b4d;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
}
/* Style for the container holding messages */
#message-display {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
min-height: 60px;
background-color: #e9ecef;
/* A light grey for the main page display */
margin-bottom: 15px;
transition: background-color 0.3s ease;
}
/* Specific style for the iframe's message display to differentiate it */
body.iframe-body #message-display {
background-color: #e0f7fa;
/* A light cyan for the iframe display */
}
/* Input and Button Grouping */
.input-group {
display: flex;
margin-bottom: 20px;
}
#message-input {
flex-grow: 1;
/* Allows the input to take up available space */
padding: 10px 15px;
font-size: 16px;
border: 1px solid #ccc;
border-right: none;
border-radius: 8px 0 0 8px;
/* Rounded corners on the left side */
}
#message-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
button {
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 0 8px 8px 0;
/* Rounded corners on the right side */
cursor: pointer;
transition: background-color 0.2s ease;
}
button:hover {
background-color: #0056b3;
}
/* Iframe specific styles */
#my-iframe {
border: 2px solid #007bff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
document.addEventListener('DOMContentLoaded', () => {
const iframe = document.getElementById('my-iframe');
const messageInput = document.getElementById('message-input');
const sendMessageBtn = document.getElementById('send-message-btn');
const messageDisplay = document.getElementById('message-display');
// 1. Create a new MessageChannel. This gives us two connected ports.
const channel = new MessageChannel();
const port1 = channel.port1;
// 2. Wait for the iframe to load before we try to communicate with it.
iframe.addEventListener('load', () => {
console.log('Main: Iframe loaded. Transferring port2.');
// 3. Transfer port2 to the iframe. This is the crucial step.
// The first argument is the message, the second is the target origin ('*' for any),
// and the third is an array of objects to transfer.
iframe.contentWindow.postMessage('init', '*', [channel.port2]);
});
// 4. Listen for messages coming back from the iframe on our port (port1).
port1.onmessage = (event) => {
console.log('Main: Message received from iframe:', event.data);
messageDisplay.textContent = `From Iframe: ${event.data}`;
};
// 5. Add a click listener to send messages to the iframe via port1.
sendMessageBtn.addEventListener('click', () => {
const message = messageInput.value;
if (message) {
console.log('Main: Sending message to iframe:', message);
port1.postMessage(message);
messageInput.value = '';
}
});
});
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('message-input');
const sendMessageBtn = document.getElementById('send-message-btn');
const messageDisplay = document.getElementById('message-display');
let port2; // This will hold the port transferred from the main page.
// 1. Listen for the initial message from the main window.
window.addEventListener('message', (event) => {
// We only want to handle the 'init' message once to get the port.
if (event.data === 'init' && event.ports[0]) {
console.log('Iframe: Port received from main page.');
// 2. Grab the port from the event.
port2 = event.ports[0];
// 3. Set up a listener on this port to receive subsequent messages.
port2.onmessage = (portEvent) => {
console.log('Iframe: Message received from main page:', portEvent.data);
messageDisplay.textContent = `From Main: ${portEvent.data}`;
};
// Optional: Send a confirmation message back to the main page.
port2.postMessage('Hello from the iframe! We are connected.');
}
});
// 4. Add a click listener to send messages back to the main page via port2.
sendMessageBtn.addEventListener('click', () => {
const message = messageInput.value;
if (message && port2) {
console.log('Iframe: Sending message to main page:', message);
port2.postMessage(message);
messageInput.value = '';
}
});
});
The Channel Messaging API gives you a simple, efficient way to set up a private, two-way pipe between two JavaScript contexts. It’s great for structured, request/response communication, streaming data, and composing modular front-end architectures without coupling everything to window references or global broadcasts.