Responsive Iframes — yes it is possible

The Web has always had a love-hate relationship with 3rd-party content.  Whether that external content is self-contained functionality brought into a website via SaaS, or to add a donation form to your website in a way that reduces your PCI Requirements, or to possibly connect your disparate web properties together.  Back in the prehistoric days before responsive web development (a.k.a. two years ago) a common way to insert 3rd-party content was with an iframe. Something like:

<iframe src="http://example.com/widget" width="300" height="500" />

The challenge with responsive design is the hard-coded width and height.  You might be able to choose reasonable settings for a desktop viewport, but a smaller mobile viewport may require something narrower and longer (or sometimes even wider and shorter).  And as the user interacts with the iframe (e.g. submits a form) the content may shrink and grow. Iframes quickly become untenable in a responsive world.  But often there is no alternative (Salsa, Blackbaud NetCommunity, Wufoo and many other CRM/donation/form platforms do not have adequate JavaScript APIs).

However there is a solution. We can use JavaScript to communicate between the iframe and the top-level page. With one slight problem — due to the same-origin policy an iframe at a different domain can't use JavaScript to directly communicate with the parent page.  Instead we have to use the postMessage() method to do cross-window messaging. This is a bit awkward, but there's good news at the end of this blog post.

postMessage()

Let's start with a quick tutorial on how postMessage() works. We'll set up a line of communication from the parent to the iframe and then send CSS and JavaScript links that can be used to style and otherwise manipulate the content.

// Parent
var iframe = document.getElementById('the-iframe');
var tags = '';
tags += '';
// Send a message to the iframe only if it contains content from
// https://frame.example.net.
iframe.contentWindow.postMessage(tags, 'https://frame.example.net');

Before we get too far there is one caveat: You must be able to have a small snippet of JavaScript in the iframe (e.g. If your donation form’s configuration tool allows you to insert JavaScript). Unfortunately many 3rd-party tools don’t allow this. Other JavaScript we can send over to the iframe via postMessage(), but we need to start with something.

// Iframe
function listener(event) {
  // Only listen to events from the top-level page.
  if (event.origin !== "http://example.com" &&
      event.origin !== "https://example.com" &&
      event.origin !== "http://dev.example.com" &&
      event.origin !== "https://dev.example.com"
  ) {
    return;
  }
  jQuery('head').append(event.data);
}
// We attach this differently for older versions of IE.
if (window.addEventListener) {
  addEventListener("message", listener, false);
}
else {
  attachEvent("onmessage", listener);
}

The cross-window messaging security model can be two-sided. The sender ensures that the data is only sent to trusted domains and the receiver checks that the message came from trusted domains.

Resizing the iframe

To resize the iframe we need to communicate in the other direction — the iframe needs to tell the parent how big it is.

// Iframe.
/**
 * Send a message to the parent frame to resize this iframe.
 */
var resizeParentFrame = function() {
  var docHeight = jQuery('body').height();
  if (docHeight != parentDocHeight) {
    parentDocHeight = docHeight;
    // There is no need to filter this to specific domains, since the data is
    // not sensitive, so just send it everywhere.
    window.parent.postMessage(parentDocHeight, "*");
  }
};

// Check frequently to see if we need to resize again.
setInterval(resizeParentFrame, 250);

// Parent.
var listener = function (event, iframe) {
  // Ignore messages from other iframes or windows.
  if (event.origin !== 'https://frames.example.net') {
    return;
  }
  // If we get an integer, that is a request to resize the window
  var intRegex = /^d+$/;
  if (intRegex.test(event.data)) {
    // Math.round() is important to make sure Internet Explorer
    // treats this as an integer rather than a string (especially
    // important if the code below were to add a value to the
    // event.data, although currently we aren't doing that).
    iframe.object.height(Math.round(event.data));
  }
}
// Setup the listener.
if (window.addEventListener) {
  addEventListener("message", listener, false);
}
else {
  attachEvent("onmessage", listener);
}

You can see these concepts in action on the Global Zero donate page. On a soon-to-be-launched project we’re also taking it one step further to pass other information to/from the iframe.

If you do get this far, you’ll note how this is a big pain in the arse; It will take several hours to get this all setup and QAed. The good news is that at some point in the future we won’t have to do this. HTML5 has a new attribute for iframes called seamless. A seamless iframe will inherit the parent’s CSS and adapt in size to fit the contents. The bad news is that it has yet to be implemented by any browser1, so it’s still a long way off.


Photo by Bruce Denis
Thanks to Ben Vinegar for confirming that I'm not crazy to think this is the best way to do this.

1 Some browsers currently have very superficial support — they only remove the default iframe border. But that’s just simple CSS that your browser-reset CSS is likely dealing with anyway.

UPDATE JAN 2014: You may want to look at an article from Previous/Next. This functionality is now available as a library.