The iframe Auto-Resize Handshake: Engineering Embeddable Widgets That Don't Fight Their Host
If you have ever embedded a third-party widget in an iframe, you know the two failure modes: an inner scrollbar that swallows half the widget, or a 2000-pixel-tall frame with 400 pixels of content. The usual fixes are ha
If you have ever embedded a third-party widget in an iframe, you know the two failure modes: an inner scrollbar that swallows half the widget, or a 2000-pixel-tall frame with 400 pixels of content. The usual fixes are hacky (hardcoded heights, scrolling=no and prayer). This post is the full engineering notes for the embed surface I run at Practical Web Tools — every calculator on the site (400+, across finance, math, health and science) is embeddable as an auto-sizing iframe, and the whole sizing layer is ~60 lines of framework-free ES5.
The handshake
The framed page measures itself; the host page applies the height. Three pieces:
Inside the frame — measure on load, on every DOM change, and on a few early intervals (cache-cold renders change height late):
var send = function (force) {
var h = Math.ceil(Math.max(
document.documentElement.offsetHeight,
document.body.offsetHeight || 0));
if (!h || (h === last)) return;
last = h;
parent.postMessage({ type: 'pwt-embed:resize', height: h }, '*');
};
new ResizeObserver(function () { send(); }).observe(document.body);
// plus: DOMContentLoaded, load, and 500ms x 40 early ticks
On the host — one listener, matched to the right frame (never document.querySelectorAll('iframe')[0]):
window.addEventListener('message', function (e) {
if (e.origin !== 'https://practicalwebtools.com') return;
if (e.data.type !== 'pwt-embed:resize') return;
var h = Math.min(Math.max(Math.round(e.data.height), 160), 20000);
// find the iframe whose contentWindow === e.source, then:
iframe.style.height = h + 'px';
});
The late-script case — if the host's resizer script loads after the frame already reported (async scripts, bottom-of-page pastes), the frame's message is gone. Fix: the host pings back {type:'pwt-embed:ping'} on load and on DOMContentLoaded, and the frame force-sends on ping. No timing assumptions in either direction.
The feedback loop that ate my afternoon
Apply the reported height → the framed page reflows → it reports a new height → apply → reflow. This happens whenever the framed document has any viewport-sized element (a min-height:100vh hero is the classic). The loop's signature: the same height delta each round. So the frame-side code tracks the delta; if |new - last| repeats five times, height reporting stops. The widget might be slightly wrong-sized at that point, but it stops growing forever, which is the difference between "a bit short" and "the page is 40,000 pixels long."
Third-party frames deny your storage
Browsers throw on localStorage access inside cross-origin iframes (Safari is the strictest). A calculator that persists its inputs — the retirement calculator keeps your balances, the GPA calculator keeps your courses — crashes on load inside a frame unless you shim it before any tool code runs:
try { localStorage.setItem('__probe', '1'); localStorage.removeItem('__probe'); }
catch (e) {
var mem = {};
window.localStorage = shim(mem); // getItem/setItem/removeItem/key/length
}
Probe, don't feature-detect once and cache: the probe itself is the detection.
Clicks, links, and not hijacking the host
Inside a frame, a plain link either traps the user in the widget or navigates the host page out from under them. Neither is acceptable. The embed bootstrap intercepts left-clicks on same-site links and rewrites them to window.open(href, '_blank', 'noopener'). Off-site links keep their authored target. The host page never navigates because of something inside the widget.
Ads and embeds don't mix
The site is ad-supported; embeds load zero ads, zero analytics trackers, zero site chrome. This is not altruism — a widget that renders ads onto someone else's article gets ripped out, and rightly so. The framed document drops to a bare tool with a two-line credit bar ("Free calculator by Practical Web Tools" + "Open full calculator"). That credit link is the entire business model, and it ships inside the snippet, in the host page's HTML, as a plain anchor — not injected by script after the fact.
Themes, honestly
"Follow the visitor's OS" paints dark widgets into light articles — the host picked their page's look, not the visitor's OS. Widgets default light and accept an explicit &theme=dark. Boring, correct.
Try it
Every calculator page (say the moon phase tool or the body-fat calculator) shows its own copy-paste snippet with a live preview at the bottom of the page; the flagship hub with the nine one-line script widgets (no iframe at all) is practicalwebtools.com/embed. The MIT-licensed scripts are on GitHub if you'd rather self-host the whole thing.
If you hit a sizing edge case I didn't — especially CSS-driven oscillation — I'd genuinely like to hear about it in the comments.
Tags: javascript, webdev, iframe, widgets
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.