21

How to make an iframe responsive, without assuming an aspect ratio? For example, the content may have any width or height, which is unknown before rendering.

Note, you can use Javascript.

Example:

<div id="iframe-container">
    <iframe/>
</div>

Size this iframe-container so that contents of it barely fit inside without extra space, in other words, there is enough space for content so it can be shown without scrolling, but no excess space. Container wraps the iframe perfectly.

This shows how to make an iframe responsive, assuming the aspect ratio of the content is 16:9. But in this question, the aspect ratio is variable.

Ferit
  • 8,692
  • 8
  • 34
  • 59
  • You mean the contents inside of the iframe? Or the iframe itself? – Travis J Apr 26 '20 at 16:56
  • 1
    Let me clarify with an edit @TravisJ – Ferit Apr 26 '20 at 17:02
  • 2
    It's possible to calculate the dimensions of the content in the iframe, but how exactly it should be done, depends a bit on the content itself, and a lot of the used CSS (absolutely positioned content is extremely hard to measure). If you're using a css reset file, the result differs from the a page the reset file is not used, and also varies between browsers. Setting the size of the iframe and its parent is trivial. So, we'd need more information about the stucture of the page, specifically the name(s) of the used CSS reset file(s) (or the actual CSS, if you're not using any common file(s)). – Teemu Apr 27 '20 at 07:58
  • You can create a proof of concept solution and show that. It shouldn't be a solution to specific implementation but a solution to this problem generally. So that everyone can benefit from the answers to this question. @Teemu – Ferit Apr 27 '20 at 09:03
  • There's no generic solution, or we can say, that the generic solution is to calculate the dimensions of the iframe content, and then use these dimensions to set the dimensions of the iframe element itself. But but ... The calculation part isn't generic, especially, if you've absolutely positioned elements inside the iframe, or some CSS stylesheet is affecting to the margins, position or display of the specific elements, like html, body or any content element. – Teemu Apr 27 '20 at 09:08
  • In that case, there is no way to make a responsive iframe container. Because the problem is, the client does not know what iframe to render, still wants to reserve just enough space for it, no less no more. – Ferit Apr 27 '20 at 11:52
  • @ferit you can have a hosted by the same origin iframe to comunicate with it's parent window and calculate the size by it's contents, but that won't work for different origin iframes. Do you want a global solution or just for iframes hosted by the same origin/domain? – Christos Lytras Apr 27 '20 at 12:03
  • I would prefer a globally working solution but having a solution working for any iframe from the same origin/domain would be helpful as well. If you have such a solution, please show. @ChristosLytras – Ferit Apr 27 '20 at 12:23
  • 2
    Do you have control over the document loaded in the iframe? I mean, if you don't, then there's nothing you can do. Everything has to be done inside the iframe document (or by accessing that document), otherwiswe this is not possible at all. – Teemu Apr 27 '20 at 12:25
  • I'm a user of iframe source, not developer, so no, I have no control over. I feel it should be possible, fingers crossed. @Teemu – Ferit Apr 27 '20 at 12:27
  • 1
    No, then it's not possible, see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy . The main page can't access a cross-domain document in the iframe (and vice-versa) for security reasons, this can be circumvented only if you have control over the document shown in the iframe. – Teemu Apr 27 '20 at 12:29
  • Perhaps, but proving that it's impossible is helpful as well, it would prevent people lose time on it. Either way, I think this bounty will be helpful for the community. Thanks for looking into this. @Teemu – Ferit Apr 27 '20 at 12:41
  • 1
    ... 15 years of internet saying it's impossible isn't enough? Do we really need a new question about this? – Kaiido Apr 27 '20 at 12:56
  • I'm drawing attention to this problem to shed some light, putting my own 500 points as a bounty, incentivizing people to find a solution. What's bad about it? If you are sure that it's impossible, you can just ignore. – Ferit Apr 27 '20 at 13:01
  • You are supposed to make you due research before posting a question. This question has already been asked at least [5 years ago](https://stackoverflow.com/questions/30856767/how-can-i-resize-a-cross-domain-iframe-that-i-do-not-have-control-over) with better details. Many similar questions have been asked where you could have inferred the same response. Making us loose our time looking at the same question over and over is "bad", and we can't even vote to close because you did post a bounty. You could put $100 on this that wouldn't make the question a better fit for the community. – Kaiido Apr 27 '20 at 13:06
  • If you're not taking what experienced users are telling you, you should read the [standard](https://html.spec.whatwg.org/#origin) "_Actors with differing origins are considered potentially hostile versus each other, and are isolated from each other to varying degrees._" Unfortunately you're just loosing your rep here without any result. – Teemu Apr 27 '20 at 13:17
  • Feel free to write your answer, explaining this is impossible. I would gladly award the bounty unless no one comes with a solution. Proving that it's impossible, as I said, also has value. @Teemu – Ferit Apr 27 '20 at 13:21
  • Well, I could, but the requirement of the cross-origin source of the iframe should be mentioned in the question itself, currently it's a bit misleading, and the information is not easy to find from the long comment thread. – Teemu Apr 27 '20 at 13:23
  • It's not misleading, the question gives no constraint about iframe source. So if unconstrained, it means source can be anything. Solving the problem with an additional constraint of same origin/domain would be a subset solution, but in case of the impossibility of a general solution, it is very helpful as well. – Ferit Apr 27 '20 at 13:27
  • @ferit I came to that question because it's about iframe, that I think I know a lot about this domain, and in the event it's a case I didn't know yet, I would have loved learning about. Now, I'm just disappointed to see it's just this question again, not even with all the required info in it, since we need to go to the comments to see that you are in the stuck case of a cross-domain page with no writing access to it. – Kaiido Apr 27 '20 at 14:27
  • No I don't care about the points, I never did in 6 years here, and it's quite insulting you think I'm commenting here for that bounty I'm complaining about because it blocks me from doing my moderation duty. If I really did care about points, I would already have posted the same answer as there: "You can't", because that's the only *correct* answer to that question. And finally, it did take me two minutes of my fair time to find that dupe. Sure I could probably have found a better one, but it's not my job, it's yours. – Kaiido Apr 27 '20 at 14:27
  • I need more info to answer you. are the content of the Iframe is from the same domain, subdomain? are you control the content of the two domains? can you inject the script inside them? – pery mimon May 02 '20 at 01:54
  • @pery mimon I would like to see what can be done if its the same domain or not. I don't think I can inject script inside them but feel free to enlighten me about it as well. I am seeking a general answer instead of a solution to a specific problem. – Ferit May 02 '20 at 12:37

3 Answers3

9

It is not possible to interact with a different origin iFrame using Javascript so to get the size of it; the only way to do it is by using window.postMessage with the targetOrigin set to your domain or the wildchar * from iFrame source. You can proxy the contents of the different origin sites and use srcdoc, but that is considered a hack and it won't work with SPAs and many other more dynamic pages.

Same origin iFrame size

Suppose we have two same origin iFrames, one of short height and fixed width:

<!-- iframe-short.html -->
<head>
  <style type="text/css">
    html, body { margin: 0 }
    body {
      width: 300px;
    }
  </style>
</head>
<body>
  <div>This is an iFrame</div>
  <span id="val">(val)</span>
</body>

and a long height iFrame:

<!-- iframe-long.html -->
<head>
  <style type="text/css">
    html, body { margin: 0 }
    #expander {
      height: 1200px; 
    }
  </style>
</head>
<body>
  <div>This is a long height iFrame Start</div>
  <span id="val">(val)</span>
  <div id="expander"></div>
  <div>This is a long height iFrame End</div>
  <span id="val">(val)</span>
</body>

We can get iFrame size on load event using iframe.contentWindow.document that we'll send to the parent window using postMessage:

<div>
  <iframe id="iframe-local" src="iframe-short.html"></iframe>
</div>
<div>
  <iframe id="iframe-long" src="iframe-long.html"></iframe>
</div>

<script>

function iframeLoad() {
  window.top.postMessage({
    iframeWidth: this.contentWindow.document.body.scrollWidth,
    iframeHeight: this.contentWindow.document.body.scrollHeight,
    params: {
      id: this.getAttribute('id')
    }
  });
}

window.addEventListener('message', ({
  data: {
    iframeWidth,
    iframeHeight,
    params: {
      id
    } = {}
  }
}) => {
  // We add 6 pixels because we have "border-width: 3px" for all the iframes

  if (iframeWidth) {
    document.getElementById(id).style.width = `${iframeWidth + 6}px`;
  }

  if (iframeHeight) {
    document.getElementById(id).style.height = `${iframeHeight + 6}px`;
  }

}, false);

document.getElementById('iframe-local').addEventListener('load', iframeLoad);
document.getElementById('iframe-long').addEventListener('load', iframeLoad);

</script>

We'll get proper width and height for both iFrames; you can check it online here and see the screenshot here.

Different origin iFrame size hack (not recomended)

The method described here is a hack and it should be used if it's absolutely necessary and there is no other way around; it won't work for most dynamic generated pages and SPAs. The method fetches the page HTML source code using a proxy to bypass CORS policy (cors-anywhere is an easy way to create a simple CORS proxy server and it has an online demo https://cors-anywhere.herokuapp.com) it then injects JS code to that HTML to use postMessage and send the size of the iFrame to the parent document. It even handles iFrame resize (combined with iFrame width: 100%) event and posts the iFrame size back to the parent.

patchIframeHtml:

A function to patch the iFrame HTML code and inject custom Javascript that will use postMessage to send the iFrame size to the parent on load and on resize. If there is a value for the origin parameter, then an HTML <base/> element will be prepended to the head using that origin URL, thus, HTML URIs like /some/resource/file.ext will get fetched properly by the origin URL inside the iFrame.

function patchIframeHtml(html, origin, params = {}) {
  // Create a DOM parser
  const parser = new DOMParser();

  // Create a document parsing the HTML as "text/html"
  const doc = parser.parseFromString(html, 'text/html');

  // Create the script element that will be injected to the iFrame
  const script = doc.createElement('script');

  // Set the script code
  script.textContent = `
    window.addEventListener('load', () => {
      // Set iFrame document "height: auto" and "overlow-y: auto",
      // so to get auto height. We set "overlow-y: auto" for demontration
      // and in usage it should be "overlow-y: hidden"
      document.body.style.height = 'auto';
      document.body.style.overflowY = 'auto';

      poseResizeMessage();
    });

    window.addEventListener('resize', poseResizeMessage);

    function poseResizeMessage() {
      window.top.postMessage({
        // iframeWidth: document.body.scrollWidth,
        iframeHeight: document.body.scrollHeight,
        // pass the params as encoded URI JSON string
        // and decode them back inside iFrame
        params: JSON.parse(decodeURIComponent('${encodeURIComponent(JSON.stringify(params))}'))
      }, '*');
    }
  `;

  // Append the custom script element to the iFrame body
  doc.body.appendChild(script);

  // If we have an origin URL,
  // create a base tag using that origin
  // and prepend it to the head
  if (origin) {
    const base = doc.createElement('base');
    base.setAttribute('href', origin);

    doc.head.prepend(base);
  }

  // Return the document altered HTML that contains the injected script
  return doc.documentElement.outerHTML;
}

getIframeHtml:

A function to get a page HTML bypassing the CORS using a proxy if useProxy param is set. There can be additional parameters that will be passed to the postMessage when sending size data.

function getIframeHtml(url, useProxy = false, params = {}) {
  return new Promise(resolve => {
    const xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function() {
      if (xhr.readyState == XMLHttpRequest.DONE) {
        // If we use a proxy,
        // set the origin so it will be placed on a base tag inside iFrame head
        let origin = useProxy && (new URL(url)).origin;

        const patchedHtml = patchIframeHtml(xhr.responseText, origin, params);
        resolve(patchedHtml);
      }
    }

    // Use cors-anywhere proxy if useProxy is set
    xhr.open('GET', useProxy ? `https://cors-anywhere.herokuapp.com/${url}` : url, true);
    xhr.send();
  });
}

The message event handler function is exactly the same as in "Same origin iFrame size".

We can now load a cross origin domain inside an iFrame with our custom JS code injected:

<!-- It's important that the iFrame must have a 100% width 
     for the resize event to work -->
<iframe id="iframe-cross" style="width: 100%"></iframe>

<script>
window.addEventListener('DOMContentLoaded', async () => {
  const crossDomainHtml = await getIframeHtml(
    'https://en.wikipedia.org/wiki/HTML', true /* useProxy */, { id: 'iframe-cross' }
  );

  // We use srcdoc attribute to set the iFrame HTML instead of a src URL
  document.getElementById('iframe-cross').setAttribute('srcdoc', crossDomainHtml);
});
</script>

And we'll get the iFrame to size to it's contents full height without any vertical scrolling even using overflow-y: auto for the iFrame body (it should be overflow-y: hidden so we don't get scrollbar flickering on resize).

You can check it online here.

Again to notice that this is a hack and it should be avoided; we cannot access Cross-Origin iFrame document nor inject any kind of things.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
  • 1
    Thanks! Great answer. – Ferit Apr 28 '20 at 15:50
  • 1
    Thank you. Unfortunately [`cors-anywhere`](https://cors-anywhere.herokuapp.com/) is down at the moment so you can't see the [demo](https://zikro.gr/dbg/so/61407418/cross-origin.html) page. I don't want to add a screenshot of the external site for copyright reasons. I'll add an other CORS proxy if that stays down for long. – Christos Lytras Apr 28 '20 at 16:44
  • 1
    @ferit, This answer is great, and you assign full bounty to this post, but why you did not check it as the correct answer by the green tick? – AmerllicA May 05 '20 at 23:29
2

Their are lots of complications with working out the size of the content in an iframe, mainly due to CSS enabling you to do things that can break how you measure the content size.

I wrote a library that takes care of all these things and also works cross domain, you might find it helpful.

https://github.com/davidjbradshaw/iframe-resizer

David Bradshaw
  • 11,859
  • 3
  • 41
  • 70
1

My solution is on GitHub and JSFiddle.

Presenting a solution for responsive iframe without aspect ratio assumption.

The goal is to resize the iframe when necessary, in other words when the window is resized. This is performed with JavaScript to get the new window size and resize the iframe in consequence.

NB: Don't forget to call the resize function after the page is loaded since the window is not resized by itself after page load.

The code

index.html

<!DOCTYPE html>
<!-- Onyr for StackOverflow -->

<html>

<head> 
    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
    <title>Responsive Iframe</title>
    <link rel="stylesheet" type="text/css" href="./style.css">
</head>

<body id="page_body">
    <h1>Responsive iframe</h1>

    <div id="video_wrapper">
        <iframe id="iframe" src="https://fr.wikipedia.org/wiki/Main_Page"></iframe>
    </div>

    <p> 
        Presenting a solution for responsive iframe without aspect
        ratio assumption.<br><br>
    </p>

    <script src="./main.js"></script>
</body>

</html>

style.css

html {
    height: 100%;
    max-height: 1000px;
}

body {
    background-color: #44474a;
    color: white;
    margin: 0;
    padding: 0;
}

#videoWrapper {
    position: relative;
    padding-top: 25px;
    padding-bottom: 100px;
    height: 0;
    margin: 10;
}
#iframe {
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

main.js

let videoWrapper = document.getElementById("video_wrapper");

let w;
let h;
let bodyWidth;
let bodyHeight;

// get window size and resize the iframe
function resizeIframeWrapper() {
    w = window.innerWidth;
    h = window.innerHeight;

    videoWrapper.style["width"] = `${w}px`;
    videoWrapper.style["height"] = `${h - 200}px`;
}

// call the resize function when windows is resized and after load
window.onload = resizeIframeWrapper;
window.onresize = resizeIframeWrapper;

I spent some time on this. I hope you will enjoy it =)

Edit This is probably the best generic solution. However, when made very small, the iframe is not given the proper size. This is specific to the iframe you are using. It is not possible to code an proper answer for this phenomenon unless you have the code of the iframe, since your code has no way to know which size is prefered by the iframe to be properly displayed.

Only some hacks like the one presented by @Christos Lytras can make the trick but it's never going to be working for every iframe. Just in a particular situation.

Onyr
  • 769
  • 5
  • 21
  • Thanks for trying! However, it's not working as expected. When the content of iframe is small, the container is still having extra space. See this: https://jsfiddle.net/6p2suv07/3/ I changed the src of the iframe. – Ferit Apr 27 '20 at 11:21
  • I'm not sure to understand your problem with "extra space". The window minimum size is 100 px width. The iframe fits the container. The layout inside your iframe is not on my control. I answered your question. Please edit yours if you want more help – Onyr Apr 27 '20 at 11:38
  • Put a picture and describe it – Onyr Apr 27 '20 at 11:42
  • iframe takes all the space that container gives to it, the thing is, when iframe content is small, it does not require all the space it can take. So, the container should give just enough space to the iframe, no less no more. – Ferit Apr 27 '20 at 11:43
  • For the height of the iframe, this is because the height of the space is limited by the window in my example. That 's this? Your example is specific to the iframe you use, I gave a general answer which should be accepted. Of course, it's possible to tweak the code so that it fits nicely with your iframe but this requires to know the code behind the iframe – Onyr Apr 27 '20 at 11:46
  • Yeah, your example is providing all the height possible to iframe. Which is what should be happening if iframe content is big, but in case of content is small your solution is giving extra height, which is the thing I'm trying to solve. I'm trying to implement a solution which takes any iframe and gives it just enough space. – Ferit Apr 27 '20 at 11:49
  • Then that's what I said. You have to know how the iframe you want to use is behaving so as to code the appropriate response. A general solution to this problem is not possible since your code has no way to know how the iframe should behave when made very small. Any solution would be iframe specific. – Onyr Apr 27 '20 at 11:51
  • This was already explained by @Teemu. I maintain I gave best possible generic answer. – Onyr Apr 27 '20 at 11:58