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!
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
Below is an inline (embedded) SVG graphic. It was loaded from an external file called
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);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
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
src attribute needs to be
<object data="signal.svg" onload="this.before((this.contentDocument.body || this.contentDocument).children);this.remove()"></object>
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);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
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
There are some nice benefits to this pattern over others we’ve used in the past:
- 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
- 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).
iframe's content into the parent document, but if anythign fails, you’ll still see the included content.
- It leaves no trace: the
iframeis deleted after it imports the content into the page. Note: you may want to give the
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
onloadhandler, 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!
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);this.remove()"></iframe>
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.
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.