testiframe.com

How to Make an Iframe Responsive (and Auto-Resize to Its Content)

Scale embeds with one line of CSS, fill the screen without the 100vh traps, and resize an iframe to fit its content, even when it lives on another domain.

Published · 10 min read
Quick answer

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.

Contentaspect-ratioNotes
YouTube, Vimeo, most video16 / 9The default for landscape players.
Vertical video (Shorts, Reels)9 / 16Add a max-width, or it gets very tall on desktop.
Square social posts1 / 1Some social embeds size themselves with a script.
Maps, dashboards4 / 3 plus min-heightmin-height wins over the ratio on narrow screens.
Cap the size as well

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:

UnitHeight it measuresGood for
vhOn mobile, usually the largest viewport (toolbars hidden)A fallback line for old browsers
svhSmallest viewport (toolbars shown)Content that must never be covered
dvhThe current viewport, updating as toolbars moveFull-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.

Preview your embed at phone, tablet and desktop sizes

Paste the URL you're framing to see how it scales at each device preset, and whether it can be framed at all.

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.

Don't use 100% or vh heights inside the child

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.

Prefer a library?

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-ratio reserves the right box before a single byte loads.
  • Auto-height content: start with a realistic height or min-height, close to the typical content height, so the correction is small.

Common scrollbar problems:

SymptomCauseFix
Inner scrollbar plus page scrollbarFrame shorter than its contentAuto-height script, with Math.ceil
A few pixels of page scroll under a full-height frameInline baseline gap or the default body margindisplay: block, body { margin: 0 }
Frame grows but never shrinksMeasuring scrollHeight, or vh units in the childMeasure the rendered height; remove vh from the child
Horizontal scrollbar inside the frameChild content wider than the frameFix 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 postMessage traffic the framed page sends to its parent. With the scripts above, add https://testiframe.com to ALLOWED_PARENTS in a test build, send {"type":"embed:hello"} from the Console's input, and watch embed:height messages 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.