Part of the Iframe Embedding & Isolation guide.
Operative rule: In the parent’s message handler, validate event.origin against an explicit allow-list before reading event.data — an unchecked listener lets any window drive your layout.
How postMessage Resizing Works
An <iframe> is a replaced element: the browser gives it a fixed default height (roughly 150px) and never expands it to fit the document inside. A Folium map, by contrast, has a genuine content height that depends on the legend, attribution, and any panels Leaflet renders. Because the map lives in a separate document context — the whole point of Iframe Embedding & Isolation — the parent cannot read the child’s scrollHeight directly when the two are on different origins. The postMessage API bridges that gap: it is the one channel the same-origin policy allows across the boundary. The child measures itself and posts a message; the parent listens and sets the iframe’s height.
postMessage is deliberately promiscuous — any script in any window can post to any other window it has a handle to. That makes the receiving side, not the sending side, responsible for trust. The parent’s message event fires for every inbound message regardless of who sent it, and event.origin is a browser-supplied string the sender cannot forge. Reading event.data before checking event.origin is the classic mistake: it lets a hostile page push arbitrary heights or malformed payloads into your handler. On the child side, a ResizeObserver on the document body turns every layout change — a legend opening, tiles loading, the viewport rotating on mobile — into a fresh height message, which keeps the frame correctly sized as it participates in responsive dashboard layouts and stays compatible with the Content-Security-Policy headers you set for embedded Folium maps.
Production-Ready Implementation
Two documents cooperate. The child snippet is injected into the Folium map before it is written to disk (Folium exposes the map’s root element for exactly this). The parent snippet runs in the host dashboard. Note how the parent hard-codes the map’s origin and refuses everything else, and how the child posts to a known targetOrigin rather than "*".
/* ---- CHILD: runs inside the Folium map document ---- */
(function () {
// The exact origin of the parent that is allowed to receive our height.
const PARENT_ORIGIN = "https://dashboard.example.com";
function postHeight() {
// scrollHeight of the documentElement is the true content height.
const height = Math.ceil(document.documentElement.scrollHeight);
// Post to a KNOWN origin, never "*", so the height leaks to no one else.
window.parent.postMessage(
{ type: "map:height", height: height },
PARENT_ORIGIN
);
}
// Re-measure whenever layout changes: tiles load, legend opens, rotate.
const observer = new ResizeObserver(postHeight);
observer.observe(document.documentElement);
// Leaflet finishes tile layout after load; post once more to settle.
window.addEventListener("load", postHeight);
})();
/* ---- PARENT: runs in the host dashboard ---- */
(function () {
// The single origin we serve the map from. Anything else is untrusted.
const MAP_ORIGIN = "https://maps.example.com";
const MAX_HEIGHT = 4000; // clamp: reject absurd values from a bad actor.
const iframe = document.getElementById("map-frame");
window.addEventListener("message", function (event) {
// 1. GATE ON ORIGIN FIRST — before touching event.data at all.
if (event.origin !== MAP_ORIGIN) return;
// 2. Validate the payload shape and the number.
const data = event.data;
if (!data || data.type !== "map:height") return;
const h = Number(data.height);
if (!Number.isFinite(h) || h <= 0 || h > MAX_HEIGHT) return;
// 3. Only now is it safe to mutate our own layout.
iframe.style.height = h + "px";
});
})();
Alternative Variants
Injecting the child script from Python at generation time
When the map is built by folium, add the resize script to the document root so it ships inside the exported HTML. This keeps the resize logic versioned with the map artifact rather than bolted on in the browser.
import folium
from folium import Element
PARENT_ORIGIN = "https://dashboard.example.com"
m = folium.Map(location=[40.71, -74.01], zoom_start=12, tiles="CartoDB positron")
resize_js = f"""
<script>
(function () {{
const PARENT_ORIGIN = "{PARENT_ORIGIN}";
function postHeight() {{
const h = Math.ceil(document.documentElement.scrollHeight);
window.parent.postMessage({{ type: "map:height", height: h }}, PARENT_ORIGIN);
}}
new ResizeObserver(postHeight).observe(document.documentElement);
window.addEventListener("load", postHeight);
}})();
</script>
"""
# Attach to the document root so it is emitted in the final HTML.
m.get_root().html.add_child(Element(resize_js))
m.save("map.html")
Message-contract reference
| Field | Direction | Rule |
|---|---|---|
event.origin |
parent checks | Must equal the map origin exactly; check first |
type |
both | Namespaced string ("map:height") to ignore unrelated messages |
height |
child → parent | Finite positive number, clamped to a sane maximum |
targetOrigin |
child sets | The parent origin, never "*" |
sandbox |
parent sets | allow-scripts is enough; allow-same-origin not required |
Verification Steps
- Frame fits content: load the dashboard and confirm the iframe grows to the map’s full height with no inner scrollbar and no clipped legend.
- Origin gate holds: in DevTools, run
window.postMessage({type:"map:height",height:9999}, "*")from the parent console pretending to be another origin, and confirm the handler ignores it (the origin will not match). - Reacts to change: toggle a Leaflet layer or rotate a mobile viewport and confirm a fresh height message resizes the frame within a frame or two.
- No wildcard leak: search the child bundle for
postMessage(and confirm the second argument is the parent origin, never"*". - Clamp works: temporarily post a height of
999999from the real child and confirm the parent rejects it againstMAX_HEIGHT.
Common Errors & Fixes
Iframe never grows past its default height
No resize channel is wired up, or the child’s postMessage is firing before the parent listener is attached. Fix: confirm the child posts on both ResizeObserver and the load event, and that the parent’s addEventListener("message", …) runs during initial script evaluation, not after a user interaction. Posting again on load covers the race where the frame finishes rendering before the parent is ready.
Handler runs but resizing is erratic or driven by the wrong frame
The parent reads event.data before checking event.origin, so messages from ad frames, extensions, or other embeds all reach the resize logic. Fix: make the very first line of the handler if (event.origin !== MAP_ORIGIN) return;, then validate the type and the numeric height. This is the operative rule — trust is established by origin, not by payload contents.
Height oscillates or the frame grows on every message
Setting the iframe height changes the parent layout, which can feed back into the child’s ResizeObserver when the two share scrolling context, producing a growth loop. Fix: post scrollHeight of the child document only (which is independent of the iframe’s own box), and debounce the child’s observer to a single requestAnimationFrame per frame so transient measurements do not accumulate.
Nothing arrives from a sandboxed Folium frame
The parent applied sandbox without allow-scripts, so the child’s script — including the postMessage call — never executes. Fix: set sandbox="allow-scripts" on the iframe. postMessage does not need allow-same-origin, so you can keep the frame cross-origin and isolated while still receiving its height.
Related
- Iframe Embedding & Isolation — parent guide on the document boundary that makes
postMessagethe only cross-origin channel - Safely Embedding Folium Maps in React Dashboards — wiring this height listener into a React component’s lifecycle
- Setting Content-Security-Policy Headers for Embedded Folium Maps — the policy that must still permit the framed map that reports its height
- Making Python-Generated Maps Responsive on Mobile — why a content-fitted frame matters most on rotating mobile viewports