HTML Includes That Work Today

Note: this post describes an experimental technique that we still need to test for performance implications. It may end up being a useful tool, and it may end up being a practice we won't recommend. Either way, it's fascinating to us that it works!

As long as I have been working on the web, I’ve desired a simple HTML-driven means of including the contents of another file directly into the page. For example, I often want to append additional HTML to a page after it is delivered, or embed the contents of an SVG file so that we can animate and style its elements. Typically here at Filament, we have achieved this embedding by either using JavaScript to fetch a file and append its contents to a particular element, or by including the file on the server side, but in many cases, neither of those approaches is quite what we want.

This week I was thinking about ways I might be able to achieve this using some of the new fetch-related markup patterns, like rel="preload", or HTML imports, but I kept coming back to the same conclusion that none of these give you easy access to the contents of the fetched file. Then I thought, perhaps a good old iframe could be a nice primitive for the pattern, assuming the browser would allow me to retrieve the iframe’s contents in the parent document. As it turns out, it sure would!

A Quick Demo: including SVG

Permalink to 'A Quick Demo: including SVG'

Below is an inline (embedded) SVG graphic. It was loaded from an external file called signal.svg.

To load and embed the SVG file, I used the following markup:

<iframe src="signal.svg" onload="this.before((this.contentDocument.body || this.contentDocument).children[0]);this.remove()"></iframe>

Despite the fact that this markup starts out as an iframe, if you inspect the graphic above using developer tools, you’ll see the SVG markup for the icon, inlined right in the HTML DOM, with no iframe element to be found. This is because the code uses an iframe to load the file, and an onload event to inject the iframe’s content just before the iframe in the HTML, before deleting the iframe itself.

The approach also works with an object element, which is commonly used to reference SVG anyway, so I think that’s particularly nice. With an object, the src attribute needs to be data instead:

<object data="signal.svg" onload="this.before((this.contentDocument.body || this.contentDocument).children[0]);this.remove()"></object>

Another demo: including another HTML file

Permalink to 'Another demo: including another HTML file'

Maybe even more useful… here’s an example that uses HTML rather than SVG!

It was loaded using the following markup:

<iframe src="/images/includespost/htmlexample.html" onload="this.before((this.contentDocument.body||this.contentDocument).children[0]);this.remove()"></iframe>

One note about this one: you might have noticed that the markup snippet checks for either the contentDocument.body or just the contentDocument. This is a check to normalize between HTML and SVG includes. It’s necessary because even though the HTML file itself contains nothing more than a paragraph element, the browser creates a full HTML document to surround that paragraph, complete with an HTML element, a head, a body, etc. So the snippet tries to get the iframe’s body element if it exists, and if not, it goes for the entire document.

Notably, if you’re importing an HTML file that contains more than one element in it, I’d recommend wrapping it all in a div to keep the iframe markup able to simply look for the first childnode in the body.

Benefits!

Permalink to 'Benefits!'

There are some nice benefits to this pattern over others we’ve used in the past:

  • It’s declarative. Unlike most custom JavaScript approaches, this one is HTML-driven and its purpose in the markup is pretty clear at a glance.
  • It works for HTML or SVG. I’m not sure what else you’d want to include like this client-side, but this covers my own wishlist at least.
  • It’s asynchronous! The content will load in without blocking page rendering, as is the nature of an iframe.
  • It’s cache-friendly. Unlike server-side embedding, this pattern lets us include an external file while allowing the file to be cached naturally for subsequent reuse. (With server-side included content, client-side caching is possible, but harder to do).
  • It will show the content regardless of whether JavaScript runs at all, since that’s what an iframe is designed to do. The JavaScript is there to move the iframe’s content into the parent document, but if anythign fails, you’ll still see the included content.
  • It leaves no trace: the iframe is deleted after it imports the content into the page. Note: you may want to give the iframe a border:0; or even safely hide it while loading (perhaps showing it again via an onerror event?).
  • It works across browsers: in my brief testing so far, this works in Chrome, Firefox, Safari, and Edge. IE will show the fallback content in the iframe, but I think I can probably get IE support to work by adjusting the JS in the onload handler, as it’s currently using syntax that IE doesn’t like. With a little tweaking, I expect IE support will be possible.
  • It could even be wrapped in a web component if you’d like, as Andy Bell has cleverly demonstrated here (which is a little cleaner markup, but a little more fragile as far as JS dependence goes).

It’s fun to think of other possible uses… Perhaps you could pull in HTML modules along with their relevant CSS link. Or embed a tweet or code examples in documentation or a blog post. It could probably even be used to load and apply a regular rel=stylesheet link asynchronously, and at a low priority, which is otherwise surprisingly hard to do (note: I didn’t test this idea much to say for sure).

Can it be lazy loaded too? Yes, soon!

Permalink to 'Can it be lazy loaded too? Yes, soon!'

Another nice thing about using an iframe for this pattern is that iframes will soon gain the native ability to lazy-load when they enter the viewport. This will be achieved using the loading="lazy" attribute, which also will also work for img elements. Here’s how that markup will look:

<iframe src="signal.svg" loading="lazy" onload="this.before((this.contentDocument.body||this.contentDocument).children[0]);this.remove()"></iframe>

Possible Drawbacks

Permalink to 'Possible Drawbacks'

Iframes are very commonly used on the web, but it’s possible that overusing them in a page could lead to a performance or memory consumption problem. It’s possible that using this for all the icons on a page would be too heavy, for example, but it could be nice to use for just the particular icons that need to be animated and styled. I’ll have to do more testing before I can say.

There’s also the possibility of XSS problems, though I’m not sure this is any different than other situations where you’d want to be careful with external content. The usual safechecks apply, and it’s probably best to consider it a same-domain technique, though again I’m not sure on that either.

For now, this approach feels promising as an improvement over prior methods we’ve used for including another file directly into a page.

Feedback?

Permalink to 'Feedback?'

We’ll be continuing to test this pattern and may post a followup soon if we find anything interesting. If you have any feedback or ideas, feel free to hit us up on twitter. Thanks for reading!

Note: This post is a followup and improvement on an earlier experiment I posted on Codepen.

All blog posts