Calculating and Communicating the height of an iframe

First-party iframes

The iframe is loaded from the same domain as the parent domain and therefore there are no cross-domain access issues.

Initial Height

An initial height of 0 can result in a sudden scrollbar ‘snap’ effect if the true height that is applied later is larger than the user’s viewport (even if you have the windows’ vertical scroll bar styled to always be visible).

Resizing feels a lot better if the initial height is downsized to the true height. In this example, I’ve used 1800px. Use a value that is greater than 95% of the typical content loaded into the iframe without getting ridiculous to cover edge cases.

1
<iframe id="clientIframe" src="/client/344394" frameBorder="0" scrolling="no" style="width:100%;height:1800px;border:none"></iframe>

When setting the initial value using either the height attribute or inside a style attribute, like the example above. It is important to then stay consistent with this choice when changing it later.

True Height

A lot of solutions to this use the scrollHeight property and are done with it - but there are problems with this. As mentioned in the MDN documentation

it includes the element padding but not its margin.

There are three different properties you can use when calculating the height of an element:

  • scrollHeight
  • offsetHeight
  • clientHeight

The best solution I have come across is to use the iframes’ document element, check the value of all three, and take the largest value (credit to James Padolsey):

1
2
3
4
5
6
var doc = document.getElementById('clientIframe').contentWindow.document,
trueHeight = Math.max(
Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
);

If you don’t care about documents in IE quirks mode you can drop the doc.body checks.

Why don’t you use jQuery?

You could use the .outerHeight() method

1
$('#clientIframe').contents().find("html").outerHeight(true);

As great as jQuery is for removing cross-browser woes, for this particular problem I stopped coming across edge cases by sticking to native JavaScript and measuring the document element (.outerHeight is not available at window and document elements).

A jQuery plugin for resizing iframes is available, though I have not tried it.

Applying the Height

Here is the earlier code inside a function that then updates the height:

1
2
3
4
5
6
7
8
9
10
11
function resizeClientIframe() {
var clientIframe = document.getElementById('clientIframe'),
doc = clientIframe.contentWindow.document,
trueHeight = Math.max(
Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
);
clientIframe.style.width = trueHeight + 'px';
}

You can then use a JavaScript library like jQuery to run this once the iframe has loaded.

1
2
3
$('#clientIframe').load(function() {
resizeClientIframe();
});

Detecting when an iframe has loaded natively is not simple. Here are some resources on this:

Older Internet Explorer’s

I have seen a number of occasions where the load event for the iframe fails to fire in IE 6 and IE 7. A fix for this is to manually execute resizeClientIframe() after a defined period of time.

1
2
3
4
5
6
7
8
9
// where isIE6orIE7 is true for these browsers
if (isIE6orIE7) {
var timer = setTimeout(resizeClientIframe, 2000);
} else {
$('#clientIframe').load(function() {
resizeClientIframe();
});
}

Changing height

If the height of the iframe can change after loading, for example clicking on tabs changes displayed content, then you could modify the solution so that it is constantly checking:

1
2
3
4
$('#clientIframe').load(function() {
resizeClientIframe();
var timer = setInterval(resizeClientIframe, 2000);
});

However, this may not produce the result you were expecting (explained in Third-party iframes).

Sub-domains

The content is served from a different sub-domain e.g. clients.mysite.com . Change document.domain to eliminate the sub-domain part:

1
document.domain = "mysite.com";

Explained further in the MDN article about same-origin policy

Third-party iframes

The content is served from a different domain. You have access to the code on both domains.

Older Internet Explorer’s

For IE 6 and IE 7 support use easyXDM to communicate between the frames.

Using postMessage()

Firstly, let’s create the page to be loaded in an iframe (located at http://iframecontent.com) with the true height code:

Iframe content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
<head>
<title>iframe content</title>
<style>
div {
background-color: green;
width: 300px;
height: 600px;
}
</style>
</head>
<body>
<div id="main"></div>
<script>
function trueHeight() {
doc = document;
return Math.max(
Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
);
}
</script>
</body>
</html>

Now let’s update this to send the true height to the parent document (located at http://parentdocument.com) using postMessage().

Parent document

The parent page loads the iframe and receives the height value from it.

event.origin is a security measure to prevent data coming from unauthorised senders. You can additionally supply the expected port number. Further details on postMessage.

Changing height revisited

Let’s modify this example to have the iframe content constantly changing the height and to regularly send the height to the parent document:

If we were to output the height value being passed to the parent document it would look something like this:

  • 678
  • 678
  • 879
  • 879
  • 879
  • 1305
  • 1305
  • 1305
  • 1305
  • 1456
  • 1456
  • 1456
  • 1456

The value passed to the parent would only update when the content reached a larger value. This flaw is caused by our setting the height of the iframe in the parent document and the function checking the true height:

1
2
3
4
5
6
7
8
function trueHeight() {
doc = document;
return Math.max(
Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight),
Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight),
Math.max(doc.body.clientHeight, doc.documentElement.clientHeight)
);
}

To show how mismatched browsers are when it comes to measuring document height here is a table of values in pixels returned by different browsers, for the resizing iframe example, where the height attribute has been hard coded to 1000px in the parent document.

Browser documentElement.scrollHeight documentElement.offsetHeight documentElement.clientHeight body.scrollHeight body.offsetHeight body.clientHeight
Firefox 23* 1000 405 1000 389 389 389
Chrome 28 700 700 1000 1000 684 684
Safari 6 365 365 1000 1000 349 349
IE 8 1000 1000 1000 549 549 549
IE 9 1000 1000 1000 387 387 387
IE 10  1000 1000 1000 871 871 871
IE 11* 1000 414 1000 398 398 398

The values in bold are the largest value for each browser. As the iframe content resizes the 1000px values only change when the content becomes larger while the smaller values correctly change.

*Firefox and IE 11 act differently in that some values will always have a minimum value of 1000px. So when the content is greater than 1000px it increases these values before reverting back to 1000px:

Firefox 23

  • Response 1: | 1000 | 405 | 1000 | 389 | 389 | 389 |
  • Response 2: | 1422 | 1422 | 1000 | 1406 | 1406 | 1406 |
  • Response 3: | 1000 | 480 | 1000 | 464 | 464 | 464 |

IE 11

  • Response 1: | 1000 | 414 | 1000 | 398 | 398 | 398 |
  • Response 2: | 1389 | 1389 | 1000 | 1373 | 1373 | 1373 |
  • Response 3: | 1000 | 913 | 1000 | 897 | 897 | 897 |

What we would hope for being returned is an output that updated when the content got smaller as well:

  • 678
  • 678
  • 497
  • 497
  • 879
  • 879
  • 523
  • 523

To fix this we need to send the current height set for the iframe in the parent document to the iframe, exclude any values that match this height (in the example above 1000) and then return the largest size still remaining.

Note: if you are do not have to support IE 8 and below I would probably re-write the code added from this point onwards.

Iframe content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
<head>
<title>iframe content</title>
<style>
div {
background-color: green;
width: 300px;
height: 600px;
}
</style>
</head>
<body>
<div id="main"></div>
<script>
var start = setInterval(changeBoxHeight, 5000);
function changeBoxHeight() {
var randomHeight = Math.floor(Math.random() * (1500 - 300 + 1)) + 300;
document.getElementById('main').style.height = randomHeight + 'px';
}
function trueHeight(parentFrameHeight) {
var doc = document,
heights = [doc.body.scrollHeight, doc.documentElement.scrollHeight, doc.body.offsetHeight, doc.documentElement.offsetHeight, doc.body.clientHeight, doc.documentElement.clientHeight ];
filteredHeights;
function removeParentHeight(values) {
var a = [];
for (var i = 0, len = values.length; i < len; i++) {
//!= operator needed for IE 8 and below as they only support strings as .postMessage's message
if (values[i] != parentFrameHeight) {
a.push( values[i] )
}
}
return a;
}
filteredHeights = removeParentHeight(heights);
return Math.max.apply(Math, filteredHeights);
}
window.addEventListener('message',function(e) {
if (e.origin === "http://parentdocument.com") {
var documentHeight = trueHeight(e.data);
parent.postMessage( documentHeight,"http://parentdocument.com");
}
}, false);
/* IE 8 and below
window.attachEvent('onmessage',function(e) {
if (e.origin === "http://parentdocument.com") {
var documentHeight = trueHeight(e.data);
parent.postMessage( documentHeight,"http://parentdocument.com");
}
}); */
</script>
</body>
</html>

Parent document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>
<head>
<title>parent document</title>
</head>
<body>
<iframe id="clientIframe" src="http://iframecontent.com/" frameBorder="0" scrolling="no" style="width:100%;height:1000px;border: 1px solid red"></iframe>
<script>
setInterval(function(){
var iframeHeight = parseInt(iframe.style.height, 10);
iframe.contentWindow.postMessage( document.getElementById('clientIframe') ,'http://iframecontent.com' );
},1500);
window.addEventListener('message',function(e) {
if (e.origin === "http://iframecontent.com") {
document.getElementById('clientIframe').style.height = e.data + 'px';
}
}, false);
/* IE 8 and below
window.attachEvent('onmessage',function(e) {
if (e.origin === "http://iframecontent.com") {
document.getElementById('clientIframe').style.height = e.data + 'px';
}
});*/
</script>
</body>
</html>

Almost complete this leaves a slight snapping effect happening:

Screenshot of iframe container snapping

We need to alter the solution in the parent document to not update if the largest returned value is said between 20 pixels of the current height.

Parent document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html>
<head>
<title>Iframe loading</title>
</head>
<body>
<iframe id="clientIframe" src="http://iframecontent.com/" frameBorder="0" scrolling="no" style="width:100%;height:1000px;border: 1px solid red"></iframe>
<script>
var iframe = document.getElementById('clientIframe'),
ignoreRange = 20,
previousHeight = 0,
min,
max;
function between(input) {
min = previousHeight - ignoreRange;
max = previousHeight + ignoreRange;
return (input >= min && input <= max);
}
function resizeClientIframe(input) {
document.getElementById('clientIframe').style.height = input + 'px';
}
function checkClientIframeHeight() {
//run first time only
previousHeight = parseInt(iframe.style.height, 10);
checkClientIframeHeight = function(input) {
if ( !between(input) ) {
resizeClientIframe(input);
previousHeight = input;
}
}
}
setInterval(function(){
var iframeHeight = parseInt(iframe.style.height, 10);
iframe.contentWindow.postMessage(iframeHeight ,'http://iframecontent.com');
},1500);
window.addEventListener('message',function(e) {
if (e.origin === "http://iframecontent.com") {
checkClientIframeHeight( parseInt(e.data, 10) );
}
}, false);
/*window.attachEvent('onmessage',function(e) {
if (e.origin === "http://iframecontent.com") {
checkClientIframeHeight( parseInt(e.data, 10) );
}
});*/
</script>
</body>
</html>

More postMessage

Mozilla have created an abstraction layer for postMessage that provides nicer syntax and a bunch of other features.

Smaller Touch devices

Iframes on smaller devices can act differently. Some can scroll; some not at all.

Iframes that take up a large amount of the viewport can be problematic. Having a visual cue like a frame border and space for a user to scroll both the iframe and the parent window can help.

The Future

There are some browser solutions to make accommodating iframe content much easier such as the sandbox and seamless attributes, covered in this post from bocoup, unfortunately browser support is marginal.