To make an iframe responsive, give it width: 100%, height: auto and a CSS aspect-ratio such as 16 / 9. For content with a variable height, a same-origin parent can measure the iframe's contentDocument. A cross-origin page must report its own height to the parent with ResizeObserver and postMessage.
How to make an iframe responsive with aspect-ratio
An iframe has no natural size. Without CSS it renders at 300×150 pixels, or at whatever fixed width and height attributes you give it. Those fixed sizes are what break on phones: a 560px YouTube embed overflows a 390px screen.
The modern fix is the CSS aspect-ratio property. Let the width follow the container and let the height follow from the ratio. Here is the classic 16:9 video embed:
<iframe class="embed-16x9"
src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
title="Product demo video"
width="560" height="315"
allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
loading="lazy"></iframe>
.embed-16x9 {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
border: 0;
}
width: 100% fills the container, and height: auto hands the height over to aspect-ratio. A 390px column gets a player about 219px tall; a 760px column gets about 428px. The width and height attributes stay as a fallback for places that ignore your CSS, such as feed readers. Your stylesheet overrides them.
aspect-ratio has worked in all modern browsers since 2021, so you don't need a wrapper element or any JavaScript for fixed-ratio content.
| Content | aspect-ratio | Notes |
|---|---|---|
| YouTube, Vimeo, most video | 16 / 9 | The default for landscape players. |
| Vertical video (Shorts, Reels) | 9 / 16 | Add a max-width, or it gets very tall on desktop. |
| Square social posts | 1 / 1 | Some social embeds size themselves with a script. |
| Maps, dashboards | 4 / 3 plus min-height | min-height wins over the ratio on narrow screens. |
Add max-width: 960px; margin-inline: auto; to the rule. Otherwise a full-width 16:9 player on a 2560px monitor grows to 1440px tall.
The legacy padding-bottom wrapper
Before aspect-ratio existed, the standard trick was a wrapper with padding-bottom set as a percentage. Vertical padding percentages are calculated from the element's width, so 56.25% (9 ÷ 16) gives a box that is always 16:9, and the iframe is stretched over it with absolute positioning.
/* Legacy: only needed for browsers without aspect-ratio support */
.embed-wrap {
position: relative;
height: 0;
padding-bottom: 56.25%; /* 16:9. Use 75% for 4:3 */
overflow: hidden;
}
.embed-wrap iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
It still works, and you'll find it in older themes and copied snippets, but new code doesn't need it. Replace the wrapper with the aspect-ratio rule when you next touch that page.
How to make an iframe 100% width and height
Full width is easy apart from two defaults. Browsers give iframes a 2px inset border, which pushes a 100% wide frame 4px past its container. And iframes are inline, so they leave a few pixels of gap below the baseline. Set border: 0 and display: block on every iframe you size.
A full-screen iframe
For a page that is nothing but an iframe, size it to the viewport:
body { margin: 0; }
.fullscreen-frame {
display: block;
width: 100%;
height: 100vh; /* fallback */
height: 100dvh; /* follows mobile browser toolbars */
border: 0;
}
The second height line matters on phones. In mobile browsers, 100vh equals the viewport with the address bar hidden, so while the bar is visible the bottom of your iframe sits behind it. The newer units, supported in current versions of all major browsers, fix that:
| Unit | Height it measures | Good for |
|---|---|---|
vh | On mobile, usually the largest viewport (toolbars hidden) | A fallback line for old browsers |
svh | Smallest viewport (toolbars shown) | Content that must never be covered |
dvh | The current viewport, updating as toolbars move | Full-screen app shells and iframes |
Why height: 100% doesn't work on an iframe
A percentage height is a percentage of the parent's height. If the parent's height is auto (sized by its content), the percentage can't be resolved and behaves like auto. For an iframe, auto means the default 150px. That's why "iframe 100% height" so often produces a thin strip.
You can give every ancestor an explicit height (html, body and each wrapper), but flexbox is simpler and also handles a header above the frame:
<div class="app">
<header class="app-bar">Reports</header>
<iframe src="https://reports.example.com/monthly" title="Monthly report"></iframe>
</div>
body { margin: 0; }
.app {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
}
.app iframe {
flex: 1;
min-height: 0;
width: 100%;
border: 0;
}
flex: 1 gives the iframe whatever height is left under the header, with no calc(100vh - 64px) to keep in sync when the header changes.
How to auto-resize an iframe to its content (same origin)
Forms, articles and widgets have a height that depends on their content, so you want the iframe to grow and shrink with it.
If the framed page has the same origin as the parent (same scheme, host and port), the parent can reach into it through contentDocument and measure it directly. srcdoc iframes count as same-origin too, unless they're sandboxed without allow-same-origin.
<iframe id="report" src="/reports/latest.html" title="Latest report"
style="display:block;width:100%;height:600px;border:0"></iframe>
<script>
const frame = document.getElementById('report');
function fit() {
const doc = frame.contentDocument;
if (!doc) return; // cross-origin frames return null
const height = doc.documentElement.getBoundingClientRect().height;
frame.style.height = Math.ceil(height) + 'px';
}
// "load" fires for every document the frame loads, so this re-attaches after navigation
frame.addEventListener('load', () => {
fit();
const RO = frame.contentWindow.ResizeObserver;
new RO(fit).observe(frame.contentDocument.documentElement);
});
</script>
ResizeObserver re-measures whenever the content changes size, such as images loading or an accordion opening. Math.ceil avoids a stray 1px scrollbar from fractional heights.
Measure the root element's rendered height, not scrollHeight. On the root element, scrollHeight is never smaller than the iframe's own viewport, so a frame sized with it can grow but never shrink.
If the framed page sets html, body { height: 100% } or uses 100vh sections, its height depends on the iframe's height, which you're setting from its height. You get a frame that never shrinks or keeps growing. Inside an auto-sized iframe, let the document's height come from its content.
Different subdomains, such as www.example.com and app.example.com, are different origins. The old document.domain workaround is deprecated, and Chrome no longer lets pages set it by default, so use the cross-origin approach below.
How to resize a cross-origin iframe to fit its content
For a cross-origin frame, the same-origin policy blocks the parent from reading the framed document: contentDocument is null and there's nothing to measure. The page inside the iframe has to report its own height, so you need to be able to add a script to it. If you can't, use a fixed height or an aspect ratio.
The recipe: the parent says hello when the iframe loads, so the child learns the parent's exact origin. The child checks that origin against an allowlist, watches its own size with ResizeObserver, and posts each new height. The parent checks who sent it, then applies it.
Child script (inside the iframe)
<script>
(() => {
// Exact origins allowed to embed this page: scheme + host (+ port), no trailing slash
const ALLOWED_PARENTS = ['https://www.example.com', 'https://staging.example.com'];
let parentOrigin = null;
let lastHeight = 0;
function sendHeight() {
if (!parentOrigin) return;
const height = Math.ceil(document.documentElement.getBoundingClientRect().height);
if (height === lastHeight) return;
lastHeight = height;
window.parent.postMessage({ type: 'embed:height', height }, parentOrigin);
}
window.addEventListener('message', (event) => {
if (event.source !== window.parent) return;
if (!ALLOWED_PARENTS.includes(event.origin)) return;
if (!event.data || event.data.type !== 'embed:hello') return;
parentOrigin = event.origin; // post heights only to this origin
lastHeight = 0;
sendHeight();
});
new ResizeObserver(sendHeight).observe(document.documentElement);
})();
</script>
Parent script (on the embedding page)
<iframe id="widget"
src="https://widget.example.net/embed"
title="Pricing calculator"
style="display:block;width:100%;height:480px;border:0"></iframe>
<script>
const CHILD_ORIGIN = 'https://widget.example.net';
const frame = document.getElementById('widget');
// Say hello each time the frame finishes loading a document
frame.addEventListener('load', () => {
frame.contentWindow.postMessage({ type: 'embed:hello' }, CHILD_ORIGIN);
});
window.addEventListener('message', (event) => {
if (event.origin !== CHILD_ORIGIN) return; // who sent it
if (event.source !== frame.contentWindow) return; // which frame sent it
const data = event.data;
if (!data || data.type !== 'embed:height') return; // what it is
const height = Number(data.height);
if (!Number.isFinite(height) || height < 0 || height > 20000) return;
frame.style.height = height + 'px';
});
</script>
Both sides pass an exact targetOrigin, so messages are only delivered to the expected origin, and both check event.origin and event.source before acting. The parent also clamps the height, and the 480px in the markup is the starting size until the first message arrives.
A widget embedded on a single known site can skip the hello and post straight to that origin. For more patterns, see how to communicate between an iframe and its parent with postMessage.
iframe-resizer packages this approach with extra edge-case handling. Version 5 changed its license, so check the terms before using it in a commercial project.
How to avoid scrollbars and layout shift
An auto-height iframe that jumps from 150px to 900px pushes everything below it down. That shift counts toward Cumulative Layout Shift (CLS) on your page unless it happens within 500ms of a user interaction. Reserve space up front:
- Fixed-ratio content:
aspect-ratioreserves the right box before a single byte loads. - Auto-height content: start with a realistic
heightormin-height, close to the typical content height, so the correction is small.
Common scrollbar problems:
| Symptom | Cause | Fix |
|---|---|---|
| Inner scrollbar plus page scrollbar | Frame shorter than its content | Auto-height script, with Math.ceil |
| A few pixels of page scroll under a full-height frame | Inline baseline gap or the default body margin | display: block, body { margin: 0 } |
| Frame grows but never shrinks | Measuring scrollHeight, or vh units in the child | Measure the rendered height; remove vh from the child |
| Horizontal scrollbar inside the frame | Child content wider than the frame | Fix the child's CSS; the frame's width is its viewport |
Note that overflow: hidden on the <iframe> element doesn't hide scrollbars inside it. Those belong to the framed document, so only the child's own CSS can change them.
How to lazy load a responsive iframe
Add loading="lazy" and the browser waits until the iframe is close to the viewport before loading it. Modern browsers support it for iframes, and it pairs well with aspect-ratio, because the space is reserved while the frame waits.
Don't lazy-load an iframe that's visible on first paint; that only delays it. For heavy embeds such as video players, a facade goes further: show a thumbnail inside a button with the same embed-16x9 class, and swap in the real iframe when the reader clicks it. Nothing moves, and the player's scripts never load for readers who don't press play. For how embeds affect page speed and rankings, see whether iframes are bad for SEO.
How to test a responsive iframe at different device sizes
Media queries inside an iframe respond to the iframe's width, not the device's. A widget that looks fine in a browser tab can still break in a 360px sidebar.
The testiframe.com iframe tester loads any URL in a real iframe with device presets: Mobile (390×844), Tablet (820×1180), Laptop (1280×800) and Desktop (1440×900), plus Fill, a custom width and height, and a rotate button. You can link straight to a test, for example https://testiframe.com/?url=https%3A%2F%2Fwidget.example.net%2Fembed. At each size, check:
- No horizontal scrollbar or cut-off content at the Mobile width.
- The page's own breakpoints kick in at the widths you expect.
- Height messages arrive. The Console tab logs the
postMessagetraffic the framed page sends to its parent. With the scripts above, addhttps://testiframe.comtoALLOWED_PARENTSin a test build, send{"type":"embed:hello"}from the Console's input, and watchembed:heightmessages appear as you switch sizes.
If the preview is blank instead, the page probably refuses to be framed. The tester's report names the header responsible, and the "refused to connect" guide explains how to fix it. For the basics of the embed markup itself, see how to embed a website in HTML.
FAQ
How do I make an iframe responsive?
Set width: 100% and height: auto on the iframe, then add a CSS aspect-ratio such as 16 / 9. The height then follows the width at every screen size. Add display: block and border: 0 to remove the default gap and border.
How do I make an iframe height fit its content?
If the framed page is on the same origin, read contentDocument and set the iframe height to the document's rendered height, re-measuring with ResizeObserver. If it's cross-origin, the framed page must measure itself and send its height to the parent with postMessage, and the parent applies it after checking the message origin.
Why is my iframe height 100% not working?
A percentage height needs a parent with a defined height. If the parent is sized by its content, 100% behaves like auto and the iframe falls back to its default 150px. Give each ancestor a height, use 100dvh on the iframe for a full-screen frame, or put it in a flex column with flex: 1.
Can I get the height of a cross-origin iframe?
No. The same-origin policy stops the parent from reading a cross-origin document, so contentDocument is null. The only way is for the framed page to report its height, usually with ResizeObserver and postMessage. If you can't add a script to that page, use a fixed height or an aspect ratio.
Does loading="lazy" work on iframes?
Yes. Modern browsers support loading="lazy" on iframes and wait until the frame is near the viewport. Reserve its space with aspect-ratio or a height, and don't lazy-load iframes visible on first load.