Minimal parent HTML
<!doctype html>
<html>
<body>
<iframe id="quickstart-frame" src="./child.html"></iframe>
<script src="./frameport.js"></script>
<script src="./parent.js"></script>
</body>
</html>
frameport is a zero-dependency library that makes
parent/iframe communication feel predictable instead of ad hoc. It
wraps window.postMessage in a small channel API so you
can move real behavior between windows without re-solving message
names, reply handling, timeout behavior, and late iframe startup
every time.
That makes it a strong fit when you want to:
This is the absolute minimum setup for a parent page and an iframe
to talk through frameport. The parent uses
lazyChannel(), the child sends one simple boot message,
and the parent performs one simple request that the child answers.
id and the same
availableMessages on both sides.
<!doctype html>
<html>
<body>
<iframe id="quickstart-frame" src="./child.html"></iframe>
<script src="./frameport.js"></script>
<script src="./parent.js"></script>
</body>
</html>
<!doctype html>
<html>
<body>
<div>Iframe page UI</div>
<script src="./frameport.js"></script>
<script src="./child.js"></script>
</body>
</html>
parent.jsconst iframe = document.getElementById("quickstart-frame");
// Parent page: define the channel before the iframe is ready.
const pendingChannel = frameport.lazyChannel({
id: "quickstart-demo",
availableMessages: ["child-ready", "get-answer"],
});
pendingChannel.onInit(async function (channel) {
// Listen for a simple boot message from the child.
channel.listen("child-ready", function (message) {
console.log("Child says:", message.payload.text);
});
// Ask the child to answer one simple request.
const response = await channel.request(
"get-answer",
{ timeout: 2000 },
{ question: "Hello from parent" }
);
console.log("Child answered:", response.payload.text);
});
// Connect the real iframe transport once contentWindow exists.
iframe.addEventListener("load", function () {
if (!iframe.contentWindow) {
return;
}
pendingChannel.init(
frameport.defaultIFrameGateway({
currentWindow: window,
targetWindow: iframe.contentWindow,
})
);
});
child.js// child.js: create the channel immediately.
const channel = frameport.createChannel({
id: "quickstart-demo",
availableMessages: ["child-ready", "get-answer"],
...frameport.defaultIFrameGateway({
currentWindow: window,
targetWindow: window.parent,
}),
});
// Answer one simple request coming from the parent.
channel.respond("get-answer", async function (payload) {
return {
text: `Child received: ${payload.question}`,
};
});
// Send one one-way message to announce the iframe is ready.
channel.send("child-ready", {
text: "Iframe booted and ready.",
});
The live example below shows the host window, the embedded iframe, and a shared event stream side by side.
These controls live in the host page and talk to the iframe through frameport.
This middle column is a real embedded
<iframe>. The host cannot call child
functions directly; all interaction goes through
postMessage and frameport.
See a combined event stream from both windows and filter by side.
These examples mirror the scenarios shown in the demo and explain what each method solves, when to reach for it, and how the pieces fit together.
createChannel() +
defaultIFrameGateway()
Use this when you already have access to both windows and want to
turn raw postMessage into a named, validated channel.
// Add this on the parent side once iframe.contentWindow exists.
const channel = frameport.createChannel({
id: "app-shell",
availableMessages: [
"parent-notify",
"child-status",
"get-profile",
"get-host-theme",
],
...frameport.defaultIFrameGateway({
currentWindow: window,
targetWindow: iframe.contentWindow,
}),
});
What it solves: it centralizes the low-level
transport wiring. Instead of every caller remembering how to
attach listeners, call postMessage, and filter the
incoming message shape, you create one channel object that owns
the browser-specific details.
How to use it: both sides of the connection
should use the same id and the same set of
availableMessages. That gives you a small contract:
if code tries to send or listen to a message name outside that
list, frameport throws instead of silently failing.
send() + listen()Use this pair for notifications and state broadcasts where the sender does not need a reply.
// Add this on the iframe side if the child should react to parent notices.
channel.listen("parent-notify", function (message) {
console.log("Parent says:", message.payload.text);
});
// Add this on the parent side when the host wants to notify the iframe.
channel.send("parent-notify", {
text: "The host theme changed to forest.",
});
What it solves: this is the clean replacement
for ad hoc event payloads passed directly through
postMessage. It works well for notifications such
as “theme changed”, “iframe booted”, or “new unread count”.
How to use it: register a listener on the side
that should react, then call send() from the side
that emits the update. The listener receives the full channel
message, including channelId,
messageName, and the typed payload, so debugging
stays straightforward.
request() + respond()Use this when one side needs data or work from the other and you want the interaction to feel like an async function call.
// Add this on the iframe side if the child owns the profile data.
channel.respond("get-profile", async function (payload) {
return {
id: payload.userId,
name: "Ada Lovelace",
role: "analysis engine operator",
};
});
// Add this on the parent side when the host needs that profile.
const response = await channel.request(
"get-profile",
{ timeout: 2000 },
{ userId: 42 }
);
console.log(response.payload.name);
What it solves: raw
postMessage has no built-in concept of “reply to
this exact request”. frameport adds a generated
requestId, matches the reply for you, and rejects
if the timeout expires.
How to use it: define the responder on the side
that owns the data, then call request() from the
caller side. This is ideal for “get profile”, “fetch app
config”, “ask the host for theme”, or any other API-like
interaction between windows.
lazyChannel()Use this when the iframe window does not exist yet, but you still want to prepare the channel logic in advance.
// Add this on the parent side when the iframe loads later than the host page.
const pendingChannel = frameport.lazyChannel({
id: "app-shell",
availableMessages: ["ready", "get-profile"],
});
pendingChannel.onInit(function (channel) {
channel.listen("ready", function () {
console.log("iframe connected");
});
});
iframe.addEventListener("load", function () {
pendingChannel.init(
frameport.defaultIFrameGateway({
currentWindow: window,
targetWindow: iframe.contentWindow,
})
);
});
What it solves: in real pages, the parent often
renders before the iframe is ready, so
iframe.contentWindow is unavailable during initial
setup. lazyChannel() lets you describe the channel
early and attach behavior before the transport exists.
How to use it: create the lazy channel with the
shared id and message names, register
onInit() callbacks, and call
init() once the iframe load event gives you a real
target window. This keeps bootstrap code simple and avoids
spreading conditional iframe checks across the app.