Cross Domain iframe Resizing

Published by Chris Coyier

<iframe>'s which display content from different domains have security measures in place to prevent all sorts of stuff. For example, you can't have JavaScript access anything inside it. It can be very frustrating, for example, if you just want to do something normal and white-hat like adjust the height of the iframe to fit the content inside it. These security measures are in place to prevent all the black-hat kind of things you could do if you did have JavaScript access to the innards of an iframe.

I've literally tried to work on different solutions for this for years and always came up short. I recently came across a solution from Kazi Manzur Rashid (about two years old now) that looks pretty solid so I thought I'd try it out. The results are the closest I've been able to come yet:

View Demo

Warning: the demo kinda freaks out WebKit browsers like Safari and Chrome, see issues below.

To those who have come before...

To do this with an iframe with source content on the same domain, you can do this. Same-domain iframes aren't subject to the same restrictions so it's far easier.

Adam Fortuna explored some options using kind of a man-in-the-middle idea. This may have been inspired by a technique by John McKerrell.

The following technique doesn't require the middle man thing though, which is why it's closer to ideal.

Prereqs

This solution presupposes that you have control over both the hosting site and the source site. You'll need to run JavaScript on both ends. So this isn't going to work for an iframe of google.com.

The Big Idea

The work-around is using hash tags in the URL to relay information back and forth. This circumvents the security restrictions. It is unlikely that this will ever break, so it's not really a "hack". You can't really do anything malicious with just a hash tag. In our case we're just reading in that information and using it to do the resizing.

The HOST Domain

Actually has the iframe on it:

<iframe id="frame-one" scrolling="no" frameborder="0" src="http://digwp.com/examples/iFrameSource/source.html" onload="FrameManager.registerFrame(this)"></iframe>

The iframe has an onload event on it, which calls a function from the FrameManager class, which we'll need to call in the <head>:

<script type="text/javascript" src="js/FrameManager.js"></script>

And here is the magical FrameManager class:

var FrameManager = {

    currentFrameId : '',
    currentFrameHeight : 0,
    lastFrameId : '',
    lastFrameHeight : 0,
    resizeTimerId : null,

    init: function() {
    
        if (FrameManager.resizeTimerId == null) {
        
            FrameManager.resizeTimerId = window.setInterval(FrameManager.resizeFrames, 500);
            
        }
        
    },

    resizeFrames: function() {
    
        FrameManager.retrieveFrameIdAndHeight();

        if ((FrameManager.currentFrameId != FrameManager.lastFrameId) || (FrameManager.currentFrameHeight != FrameManager.lastFrameHeight)) {
            
            var iframe = document.getElementById(FrameManager.currentFrameId.toString());

            if (iframe == null) return;

            iframe.style.height = FrameManager.currentFrameHeight.toString() + "px";

            FrameManager.lastFrameId = FrameManager.currentFrameId;
            FrameManager.lastFrameHeight = FrameManager.currentFrameHeight;
            window.location.hash = '';
            
        }
        
    },

    retrieveFrameIdAndHeight: function() {
    
        if (window.location.hash.length == 0) return;

        var hashValue = window.location.hash.substring(1);

        if ((hashValue == null) || (hashValue.length == 0)) return;

        var pairs = hashValue.split('&');

        if ((pairs != null) && (pairs.length > 0)) {
        
            for(var i = 0; i < pairs.length; i++) {
            
                var pair = pairs[i].split('=');

                if ((pair != null) && (pair.length > 0)) {
                
                    if (pair[0] == 'frameId') {
                    
                        if ((pair[1] != null) && (pair[1].length > 0)) {
                        
                            FrameManager.currentFrameId = pair[1];
                        }
                    } else if (pair[0] == 'height') {
                    
                        var height = parseInt(pair[1]);

                        if (!isNaN(height)) {
                        
                            FrameManager.currentFrameHeight = height;
                            FrameManager.currentFrameHeight += 15;
                            
                        }
                    }
                }
            }
        }
        
    },

    registerFrame: function(frame) {
    
        var currentLocation = location.href;
        var hashIndex = currentLocation.indexOf('#');

        if (hashIndex > -1) {
        
            currentLocation = currentLocation.substring(0, hashIndex);
        }

        frame.contentWindow.location = frame.src + '?frameId=' + frame.id + '#' + currentLocation;
        
    }
};

window.setTimeout(FrameManager.init, 300);

The SOURCE site

The source content could pretty much be anything, located on a different server. Perhaps:

<body>

    <div id="content">
        Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce in tortor sit amet sem luctus ornare. Nam sed augue id erat commodo gravida. Nulla in pede. Nunc sed elit non pede aliquam eleifend. Cras varius. Sed non lorem eget ipsum accumsan suscipit. Donec bibendum enim. Phasellus a ligula. Fusce turpis diam, ultricies at, ullamcorper a, consectetuer et, mauris. Pellentesque neque felis, scelerisque non, vestibulum at, luctus quis, velit. Quisque sit amet mi sed sem facilisis ornare. In leo ante, hendrerit nec, lobortis eget, feugiat ac, orci.
    </div>
    
</body>

The most important thing we do on the source site is run some JavaScript to "publish" the height of itself. In my demo, I'm also throwing some jQuery in there to do some font-size animation so that the source content grows taller and shorter.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js?ver=1.3.2"></script>
<script type="text/javascript" src="frame.js"></script>
<script type="text/javascript">

    window.onload = function(event) {
        window.setInterval(publishHeight, 300);
    }
    
    $(function() {
    
        var $content = $("#content");
    
        function toggleFontSize() {
        
            if ($content.css("font-size") == "22px") {
                $("#content").animate({
                    fontSize: "15px"
                });
            } else {
                $("#content").animate({
                    fontSize: "22px"
                });
            }
        
        }
        
        var int = setInterval(toggleFontSize, 5000);
    
    });
</script>

So we are calling publishHeight() every 300 milliseconds. Here is that function, and it's rag-tag gang of fellow supporting functions from the frame.js file.

function publishHeight() {

    if (window.location.hash.length == 0) return;

    var frameId = getFrameId();

    if (frameId == '') return;

    var actualHeight = getBodyHeight();
    var currentHeight = getViewPortHeight();

    if  (Math.abs(actualHeight - currentHeight) > 15) {
        var hostUrl = window.location.hash.substring(1);

        hostUrl += "#";
        hostUrl += 'frameId=' + frameId;
        hostUrl += '&';
        hostUrl += 'height=' + actualHeight.toString();

        window.top.location = hostUrl;
    }
}

function getFrameId() {

    var qs = parseQueryString(window.location.href);
    var frameId = qs["frameId"];

    var hashIndex = frameId.indexOf('#');

    if (hashIndex > -1) {
        frameId = frameId.substring(0, hashIndex);
    }

    return frameId;
    
}

function getBodyHeight() {

    var height,
        scrollHeight,
        offsetHeight;

    if (document.height) {
    
        height = document.height;
        
    } else if (document.body) {
    
        if (document.body.scrollHeight) {
            height = scrollHeight = document.body.scrollHeight;
        }
        
        if (document.body.offsetHeight) {
            height = offsetHeight = document.body.offsetHeight;
        }
        
        if (scrollHeight && offsetHeight) {
            height = Math.max(scrollHeight, offsetHeight);
        }
    }

    return height;
}

function getViewPortHeight() {

    var height = 0;

    if (window.innerHeight) {
        height = window.innerHeight - 18;
    } else if ((document.documentElement) && (document.documentElement.clientHeight)) {
        height = document.documentElement.clientHeight;
    } else if ((document.body) && (document.body.clientHeight)) {
        height = document.body.clientHeight;
    }

    return height;
    
}

function parseQueryString(url) {

    url = new String(url);
    
    var queryStringValues = new Object(),
        querystring = url.substring((url.indexOf('?') + 1), url.length),
        querystringSplit = querystring.split('&');

    for (i = 0; i < querystringSplit.length; i++) {
        var pair = querystringSplit[i].split('='),
            name = pair[0],
            value = pair[1];

        queryStringValues[name] = value;
    }

    return queryStringValues;
    
}

Issues

  • Refresh happy in WebKit. Apparently it used to be Firefox that got all refresh happy, but apparently with the latest version it is flip flopped. Watch out of visiting the demo in Safari or Chrome, it's a little choppy. If anyone has any ideas here, this is probably the biggest problem.
  • Messes up back button. Because of all the hash tags flying around on the host page, it may screw up the back button functionality on that page.
  • Intervals, intervals, intervals. There are a lot of intervals flying around here which are nearly always hacky-red-flags. The quicker the intervals, the smoother but more resource intensive. The slower, the more choppy but easier. Either way, sucky.
  • Limit of information sent via hash. If you were thinking about using this technique to send other information, because it happens via URL, you are limited by the amount of information that can pass. Presumably the same as a GET request... around 1k.

The Holy Grail

I think the reason I'm so obsessed with this because Wufoo forms seem to handle this so perfectly. Wufoo forms used to only be embeddable with iframes. I always had to set the height of them literally almost 50% taller than the content itself to accommodate for the innards growing when the form was submitted with errors (the error messaging expanded the height). If I didn't, the submit button would get cut off making the form un-submittable.

Wufoo now has a JavaScript embed option, but ultimately the form still comes in via iframe. However they do it, the iframe can magically resize itself as needed. I have no idea how it's done, but I imagine it something somewhat similar to what we are doing here. Because Wufoo has access to both the host page and the source page. My best guess is that the JavaScript on the host page can send requests back to the source page which can somehow accurately tell the host page what height it should be.

Got better?

It's a lot of code, but hey, it works (again, thanks for Kazi for the smarts). Got better? Please, share.

Update: David Bradshaw released iframe-resizer:

A simple library for cross domain sizing iFrames to content with support for window resizing and multiple iFrames.

Which works in any browser that supports postMessage (IE 8+)