6 June 2012

Learning to live with the abominable <iframe>

iframe HTML tags are riddled with user experience issues. It is unclear where the content comes from. Their height does not adapt to their content, forcing the user to scroll. Links open in the iframe but do not change the browser address bar location. And they're here to stay – HTML5 removes traditional framesets, but iframe is still around.

Simply put, iframes are best avoided wherever possible. Sometimes, however, they are the only available solution.

Scenario

Consider this: A large organisation uses a CMS to manage its web presence. One department in this organisation requires additional features for its website that are not available in the CMS. Although the CMS supports custom extensions, the organisation's IT department wisely wants to avoid development effort and support responsibility for additional features that are specific to one department.

Instead, the IT department introduces an iframe extension into the CMS. This allows the tech-savvy department to display additional content without the limitations of the CMS. It also makes the department free to choose the technology for its custom features.

In this case, the department decides to create a Django project that contains all its custom features.

This is a scenario that I have worked within for years. In the process, I have learnt a few things I would like to share. While the specific implementation is done in Django, the approaches discussed can be implemented in any framework.

The goal is to integrate the pages created by the Django project into the overall site as seamlessly as possible. This is not meant to trick the user - the presented content is intended as part of the page content and is only displayed in an iframe for technical reasons. We need to work around several issues that make content in an iframe so awkward to use.

Automatic height adjustment

An iframe displays its content within a fixed width and height. This is not well suited to the default web page behaviour of expanding vertically as needed. Normally, if the content of an iframe overflows the available height, the iframe displays a scrollbar. While this allows the user to access the content, it makes it obvious that the displayed content is somehow different.

Fortunately, it is relatively straightforward to work around this problem. Using JavaScript, we can adjust the height of the iframe to fit its content.

The parent page should use the following code to embed the iframed content.

<iframe src="iframed_src" onload="this.style.height = (this.contentWindow.document.body.scrollHeight) + 'px';" scroll="auto"></iframe>

As soon as the page within the iframe finishes loading, the parent page adjusts the height of the iframe to the height of the iframed document. The iframe is configured to show scrollbars only when the iframed content does not fit in the available space. As soon as the iframe is resized, the content fits neatly and the scrollbars disappear.

There are a number of important caveats to this approach:

Cross-domain policy issues

Modern browsers have strict cross-domain policies governing what information is shared between iframe and parent page. If possible, the iframed content should be served on the same domain as the parent content.

One case of cross-domain communication that can be enabled easily is where the parent page and the iframe content are served from different subdomains of the same second-level domain. In this case, both parent page and iframed page need to include a short script that explicitly allows communication across subdomains. Assuming the parent page is hosted on www.commondomain.com and the iframe content is hosted on custom.commondomain.com, both pages include the following:

<script type="text/javascript" charset="utf-8">
	try {
		document.domain = 'commondomain.com';
	}
	catch (e) {}
</script>

Alex Sexton gives a comprehensive overview of available cross-domain workarounds in a screencast.

Outer margin fail

The iframed page must ensure that there are no margins between its body and its elements, as this breaks the accuracy of the height calculation in some browsers (Firefox, for the most part). If the calculation is incorrect, the scrollbars do not disappear. Note that it is not sufficient to just have a wrapper element without margins - all elements that are aligned with any edge of the page need to have no margin towards the edge.

Flashing scrollbar

It is possible that the iframe briefly displays a scrollbar until the resize occurs.

One way to avoid this behaviour is to disable the scrollbar for the iframe altogether (using the attribute scroll="no"). However, this means that if the user has JavaScript disabled or if the height calculation fails for whatever reason, the iframed content will get cut off and becomes inaccessible.

Ultimately, the flashing scrollbar is one of the UX tradeoffs we need to accept when using an iframe.

Direct links to application states

The number of available pages within our custom content is dynamic and potentially unrestricted. It is therefore impossible to create a container page for each of the application states.

Instead, the iframe CMS extension is used in a few select places on the site, serving as an entry point for a whole feature. Therefore, a method is required to pass parameters from the parent page to the iframe.

Fortunately, HTTP requests contain the HTTP_REFERER header, which points to the location where the request originated. In the case of requests made through an iframe, this header contains the location of the parent page.

We can use this information to link to arbitrary locations within the iframe. We can append a GET query parameter of our choice to the URL of the parent page. This does not affect the behaviour of the parent page. The query parameter we have chosen is not used by the parent site and is therefore ignored. The same page and the same iframe instance are shown.

Our custom extension, however, can inspect the HTTP_REFERER and look for the query parameter. If found, it is re-interpreted as part of the requested URL. Additionally, we can inspect the referer URL itself. As each iframe serves as an entry point to a feature within the application, the referer URL can also be used to determine what content has been requested. In Django, this behaviour is best implemented as a Middleware object.

The UX tradeoff involved in this approach is the introduction of a query parameter into the URL, which makes it less clean. This is a common tradeoff many sites apparently are willing to make. One example is the wave of high profile websites like Twitter which use the #! fragment identifier as parameter for an XHR request.

Example

The referer header of the incoming request is set to the following: http://domain.com/page_containing_iframe/?id=/page/2

The URL is inspected and divided up as follows:

http://domain.com
/page_containing_iframe
/?id=
/page/2
Base domain for external site Maps to a specific app in the project Inspected query parameter Django request path

If for example, we know that /page_containing_iframe is meant to serve to serve /awesome_feature views, the request URL would be reinterpreted as /awesome_feature/page/2.

Creating links

Another problem of iframes is their link behaviour. By default, links open within the iframe. Only the iframe region is refreshed, while the location in the browser address bar remains unchanged. This destroys the illusion that the iframed content is part of the overall website.

The ability to link to arbitrary content that was introduced above is the basis for another workaround. Anchor tags support the target="_top" attribute. This forces the link to be opened as the main new location, even when clicked within an iframe. This is our desired behaviour, as it updates the address bar and refreshes the whole page.

What we require are anchors that link to the correct location in the parent site, contain the necessary query parameter that links to specific content within the Django project and include the target="_top" attribute. If all three aspects are set correctly, the link behaviour becomes virtually undistinguishable from normal links.

In Django, the reverse URL resolver and the url template tag are used to create links to content. These helper methods can be updated to create the necessary URLs and anchor tags.

django-iframed

I have created a Django app that combines the discussed middleware, reverse URL resolver and url template tag.

django-iframed makes the process of running in an iframe mainly transparent to the rest of the Django project. Referer headers of incoming requests are inspected and rewritten as if the request had been made directly. URLs and anchor tags that are routed via the embedding site are constructed automatically. The only change required is to import the respective module or template tag library.

The project is available on Github.

You can see the strategies and the project in use at the WU Library website, for instance here and here.

Summary

Use of iframe tags to embed content should never be first choice. However, using the strategies laid out above, we can work around many of the user experience issues involved. Solutions are proposed for problem areas; automatic height adjustment, arbitrary content URLs, and link behaviour. A Django-specific solution is available as a ready-to-use app, while the strategies involved can be applied in any framework.

Comments

Comments are closed.

Reactions

Pingbacks

Pingbacks are closed.

Other articles

Previous article: