Embedding a dynamic web application inside an iframe sounds simple until the embedded page changes height.
A photo gallery loads more images. A comment panel opens. A video expands. A shop section becomes visible.
The iframe itself does not automatically tell the parent page that its content is now 2,000 pixels taller.
The usual first attempt is something like this:
<iframe
src="https://embed.example.com/gallery/123"
style="width:100%;height:800px"
></iframe>
That works until the content becomes shorter or taller.
Then you get scrollbars, large empty areas, clipped content, or a mobile layout that feels completely broken.
For cross-origin embeds, the parent page also cannot simply inspect the iframe DOM because of the browser's same-origin policy.
The right solution is usually a small messaging protocol built on window.postMessage().
Why direct DOM access fails
If the parent page and iframe are served from different origins, this is blocked:
const iframe = document.querySelector("iframe");
const height =
iframe.contentWindow.document.body.scrollHeight;
For example:
Parent:
https://photographer.example
Iframe:
https://gallery.example
These are different origins.
That separation is intentional. Without it, any website could embed your banking dashboard and inspect its DOM.
So instead of reading the embedded page directly, the iframe must measure itself and explicitly send information to its parent.
Measure inside the iframe
The embedded application has full access to its own layout.
A simple implementation can measure the document height:
function getDocumentHeight() {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
}
Then send it to the parent:
window.parent.postMessage(
{
type: "embed:resize",
height: getDocumentHeight()
},
"https://photographer.example"
);
The parent listens for the event:
window.addEventListener("message", event => {
if (event.origin !== "https://gallery.example") {
return;
}
const data = event.data;
if (data?.type !== "embed:resize") {
return;
}
const iframe = document.querySelector(
'[data-gallery-embed="123"]'
);
iframe.style.height = `${data.height}px`;
});
This gives the parent exactly the information it needs without exposing the iframe DOM.
Do not use "*" as the target origin
You will often see examples like this:
window.parent.postMessage(
{
type: "embed:resize",
height: 1200
},
"*"
);
It is convenient.
It is also unnecessarily broad.
If you know which parent origins are allowed to embed your application, send messages only to those origins.
Similarly, the parent should always validate:
event.origin
before trusting a message.
Without that check, another window could send a forged message such as:
{
type: "embed:resize",
height: 999999
}
and potentially disrupt your page layout.
For more sensitive message types, origin validation becomes even more important.
ResizeObserver is better than polling
A naive embed implementation might send the current height every second:
setInterval(() => {
sendHeight();
}, 1000);
That works, but it is wasteful and can still feel delayed.
A better option is ResizeObserver.
const observer = new ResizeObserver(() => {
sendHeight();
});
observer.observe(document.body);
Now the iframe reacts when its layout changes.
A complete version might look like this:
const parentOrigin =
new URL(document.referrer).origin;
function sendHeight() {
const height = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
window.parent.postMessage(
{
type: "embed:resize",
height
},
parentOrigin
);
}
const observer = new ResizeObserver(() => {
sendHeight();
});
observer.observe(document.body);
window.addEventListener("load", sendHeight);
This catches both the initial layout and later changes.
Images create another timing problem
Media-heavy embeds often render before all images have loaded.
The first height might be:
620px
Then images appear and the real height becomes:
1840px
If image dimensions are not reserved in advance, the embedded page may resize repeatedly.
You should still use explicit image dimensions or aspect-ratio inside the iframe:
.gallery-image {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
}
That reduces layout instability and makes resize messages less noisy.
The embed protocol should not be used as a substitute for good internal layout behavior.
Add a message namespace
As your embed grows, resize messages are rarely the only thing crossing the iframe boundary.
You may eventually need:
embed:ready
embed:resize
embed:navigate
embed:checkout
embed:auth-expired
A predictable message schema helps:
type EmbedMessage =
| {
type: "embed:ready";
version: 1;
}
| {
type: "embed:resize";
height: number;
}
| {
type: "embed:navigate";
path: string;
};
Versioning is useful if customers keep old embed snippets on their websites for years.
Without versioning, changing message semantics later can break existing integrations.
Validate message payloads
Origin validation is only half the job.
Do not trust the payload either.
This is unsafe:
iframe.style.height =
`${event.data.height}px`;
Validate the value:
const height = Number(event.data.height);
if (
!Number.isFinite(height) ||
height < 100 ||
height > 20000
) {
return;
}
iframe.style.height = `${height}px`;
The exact limits depend on your product, but bounded input is much safer than blindly applying whatever arrives.
Multiple embeds need unique identifiers
A page may contain several embedded galleries.
If every resize message just says:
{
type: "embed:resize",
height: 1200
}
the parent does not know which iframe sent it.
Include an embed ID:
window.parent.postMessage(
{
type: "embed:resize",
embedId: "gallery_123",
height: 1200
},
parentOrigin
);
Then resolve the correct element:
const iframe = document.querySelector(
`[data-embed-id="${data.embedId}"]`
);
Even better, compare the source window:
if (event.source !== iframe.contentWindow) {
return;
}
This prevents one embed from accidentally controlling another.
Avoid redirect surprises
Another subtle problem appears when the iframe navigates internally.
Suppose the embedded application redirects from:
https://gallery.example/embed/123
to:
https://auth.gallery.example/login
The origin has changed.
If your parent only accepts messages from:
https://gallery.example
the resize protocol suddenly stops working.
You should either keep embedded navigation on one origin or explicitly support every trusted origin involved in the flow.
Do not solve this by accepting every origin.
Use sandbox deliberately
If you control the embed snippet, consider an iframe sandbox policy:
<iframe
src="https://gallery.example/embed/123"
sandbox="
allow-scripts
allow-forms
allow-same-origin
allow-popups
"
></iframe>
Only enable capabilities the embedded application actually needs.
Be careful with combinations such as:
allow-scripts
allow-same-origin
when embedding content from the same origin as the parent, because that can weaken the protection sandboxing was supposed to provide.
Cross-origin SaaS embeds are usually easier to reason about.
The protocol is the real integration surface
An iframe embed is not just a rectangle containing another website.
Once the embedded content needs to resize, report state, open checkout flows, or communicate navigation events, the boundary between parent and iframe becomes an API.
Treat it like one.
Define message types.
Validate origins.
Validate payloads.
Version the protocol.
Identify individual embeds.
Use browser observers instead of polling.
And keep authorization decisions on the server rather than trusting anything sent through postMessage.
With those pieces in place, a cross-origin iframe can behave like a native part of the host website without sacrificing the browser security boundary that made the iframe useful in the first place.
Top comments (0)