Experiments in HTML includes
I was curious to see what could be done today to create a site without any server side code, no build process, but still allow you to modularize parts of your layout. I'm certainly not the first one to write about this but I wanted to use this post as means to getting more familiar with the various options.
In the post I'm going to explore the following:
- iframe element
- object element
- Using fetch
- Using Custom Elements
iframe #
Lets start with the obvious. The iframe has been around forever and is supported by every major browser. It can be leveraged as an HTML include, but not without some help. First, let's look at what using an iframe looks like out of the box. Take this example below. It's including this html file which just contains a fragment of HTML and no included styles:
<body>
<iframe src="/path/to/header.html" width="100%"></iframe>
<main>Your page specific content</main>
</body>
My main content
In the demo you'll notice a few things:
- Page styles do not get applied to content inside the iframe.
- The iframe itself has some terrible default styling. By default it has a set dimension of 300px width by 150px height, and also comes with a 2px border. For the purposes of this demo I gave it a new 100% width.
- And depening on your internet connection, it could take a quick, but noticeable, moment to load the content. This is not great for something like a header that it is the first thing users will see on the page.
The first and last issues are due to the fact that the iframe loads content into a completely separate Browing Context. Basically a web page within a web page. Setting up that context and loading the resource contributes to that delay, and it being isolated from the parent page cause the lack of styling.
We can fix the styling issues without JavaScript by including styles within the html file being loaded. But that comes with it's own pros and cons:
Pros:
- We get scoped styles for our component without any build steps or JavaScript. That's pretty cool.
Cons:
- We need to repeat some of the base styles from the page. This could be negligible depending on the size and complexity of your site, but even if so, it still feels wrong to have to repeat CSS.
- Speaking of repeating styles... If you plan on using the HTML more than once on a page, you'll be repeating all of the CSS it comes with as well.
- iframes can't auto-size themsevles based on their content so without JS we need to give it dimensions. Widths are less of an issue as setting it to
100%
covers most cases, it's the height that requires some more planning, especially when it comes to different screen sizes.
Here's that same example from above but with styles included within the source and some dimensions given to the iframe:
<body>
<style>
iframe { border: none; width: 100%; height: 60px; }
/* should also use @media to change height for different screen sizes */
</style>
<iframe src="/path/to/header-with-css.html"></iframe>
<main>Your page specific content</main>
</body>
My main content
Much nicer now. But we had to repeat styles and we had to know the exact dimensions of the header to get it to look good. Let's take it to the next level using JavaScript. I saw an interesting solution on css-tricks, which actually credits this post. Basically, when html within the iframe is loaded, we use JS to grab the markup and replace the iframe that loaded it with that markup. This allows the styles of the page to get applied to it.
<body>
<iframe src="/path/to/header.html" onload="swap(this);" width="100%" style="border: none"></iframe>
<main>Your page specific content</main>
<script>
function swap(iframe) {
// if the src is an SVG it will not have .body so we need to check for both
const root = iframe.contentDocument.body || iframe.contentDocument;
Array.from(root.children).forEach((child) => {
iframe.parentElement.insertBefore(child, iframe);
});
iframe.remove();
}
</script>
</body>
My main content
Much better in terms of developer experience because we don't have to repeat styles any more. But still not great becuase we get a flash of unstyled content and layout shift while it's being loaded.
object #
Another option is the <object>
element, which is very similar to an iframe. The one added perk is that you can put fallback/loading content in between the object tag that will render immediately until the data is finished loading.
<body>
<object
type="text/html"
data="/path/to/header.html"
width="100%"
onload="swap(this);"
>
<span>loading...</span>
</object>
<main>Your page specific content</main>
</body>
My main content
Pretty much the same result as the iframe with the same limitations.
A quick note about using iframe and object before we continue on to the next section. I would use them sparingly because they are not lightweight. You need to think of them as if the were whole browser tabs within your page. Although that being said, any site that has google ads and social media buttons is most likely loading those in iframes. For example, theverge.com had 24 iframes on their home page last I checked (with ad-blocker turned off), so maybe it's not that bad if your site has a few iframes here and there.
Also, I didn't even get into the accessibility of iframes and objects, but you can read more about that in this article: https://www.tempertemper.net/blog/using-iframes-to-embed-arbitrary-content-is-probably-a-bad-idea
Fetch #
This is a simple JavaScript solution, similar to how we were swapping out the iframe content in the example above, except that we are using fetch
to retrieve our HTML fragment. In this example below I'm using an htmx
like approach with the data attribute indicating the resource to fetch and swapping out the element with the fetched content:
<body>
<div data-get="/path/to/header.html">
<!-- include fallback or loading content here -->
</div>
<main>Your page specific content</main>
<script>
document.querySelectorAll("[data-get]").forEach((target) => {
fetch(target.dataset.get)
.then((response) => response.text())
.then((text) => {
const range = document.createRange();
range.selectNode(target.parentElement);
const documentFragment = range.createContextualFragment(domString);
target.parentElement.replaceChild(documentFragment, target);
})
.catch(e => {
// handle error
console.warn(e);
})
});
</script>
</body>
My main content
2 things to note;
- this is faster than the iframe because we don't have to intialize the browser context on top of fetching the resource
- notice the use of
Range.createContextualFragment
. This methods parses and executes any JavaScript within the fragment.innerHTML
andouterHTML
do not do that.
Custom Elements #
Almost exactly like the fetch example above, but wrapping it in a custom element instead:
<body>
<x-include src="/path/to/header.html"></x-include>
<main>Your page specific content</main>
<script>
class CustomInclude extends HTMLElement {
constructor() {
super();
}
parse(domString) {
const range = document.createRange();
range.selectNode(this);
const documentFragment = range.createContextualFragment(domString);
return documentFragment;
}
getSrc(src) {
fetch(src)
.then((response) => response.text())
.then((domString) => {
const domFrag = this.parse(domString, this);
this.innerHTML = '';
this.appendChild(domFrag);
})
.catch(e => { console.warn(e)})
}
connectedCallback() {
const src = this.getAttribute("src");
this.getSrc(src);
}
}
window.customElements.define("x-include", CustomInclude);
</script>
</body>
My main content
Conclusion #
iframes and objects are decent fallback solutions, especially if you need to support older browsers, or if you prefer to have non-JavaScript solutions.
Custom Elements are an improvement but they still need JavaScript to work.
Ideally we need something like the iframe but without the overhead of all of that browsing context setup and isolation. There is an active discussion about adding something to the spec, but it's still in very early stages.