Why a standard way to load HTML matters
Every major web resource type has a natural, declarative loading mechanism: JavaScript has <script src>, CSS has <link rel="stylesheet">, images have <img>. HTML is the exception. The common workarounds each have significant drawbacks:
<iframe>— reliable but heavy; the content lives in a separate context, making sizing, scripting, and styling awkward.- AJAX — requires JavaScript just to fetch markup, which feels wrong for the web's most basic content.
- Hacks — embedding HTML inside strings or script comments is a maintenance nightmare.
HTML Imports, part of the Web Components family of specifications, directly address this gap. An import is an HTML document that can be pulled into another document, carrying not just markup but also associated CSS and JavaScript. This gives you a clean packaging mechanism for related front-end resources.
Basics of imports
An import is declared with a standard <link> element using rel="import". The URL referenced is called the import location. For cross-domain imports, the target must be CORS-enabled.
<head>
<link rel="import" href="https://web.dev/path/to/imports/stuff.html">
</head>
<!-- Resources on other origins must be CORS-enabled. -->
<link rel="import" href="http://example.com/elements.html">
To detect browser support, check whether the link element exposes an .import property. Chrome has supported imports since version 31; other vendors have deferred widespread implementation while ES Modules mature. For unsupported browsers, the webcomponents.js polyfill fills the gap.
function supportsImports() {
return 'import' in document.createElement('link');
}
if (supportsImports()) {
// Good to go!
} else {
// Use other libraries/require systems to load files.
}
Packaging related resources
Imports create a convention for bundling HTML, CSS, and JavaScript into one deliverable. A theme, a library, or even an entire application can be exposed through a single URL. Consider Bootstrap's current distribution: it requires separate CSS, JavaScript, and font files plus a jQuery dependency, and developers typically just download everything anyway. With imports, users reference one file and get the complete package:
<head>
<link rel="import" href="bootstrap.html">
</head>
The actual import file then loads Bootstrap's pieces and wires them together:
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fonts.css">
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="bootstrap-tooltip.js"></script>
<script src="bootstrap-dropdown.js"></script>
...
<!-- scaffolding markup -->
<template>
...
</template>
The importing page needs no further configuration—all dependencies are handled inside the import document.
Load events and dynamic creation
Imports begin fetching immediately; resource errors surface as onerror events on the link element. Because a failed import can leave you debugging silently, attaching load handlers is good practice:
<script>
function handleLoad(e) {
console.log('Loaded import: ' + e.target.href);
}
function handleError(e) {
console.log('Error loading import: ' + e.target.href);
}
</script>
<link rel="import" href="file.html"
onload="handleLoad(event)" onerror="handleError(event)">
var link = document.createElement('link');
link.rel = 'import';
// link.setAttribute('async', ''); // make it async!
link.href = 'file.html';
link.onload = function(e) {...};
link.onerror = function(e) {...};
document.head.appendChild(link);
Working with import content
Declaring an import does not automatically inject content into your page—it fetches a document you can then manipulate. The fetched content is accessible through the link element's .import property, and you can work with it using standard DOM APIs.
var content = document.querySelector('link[rel="import"]').import;
Note that .import will be null if the browser lacks import support, the link lacks rel="import", the link isn't attached to the DOM, or the resource isn't CORS-enabled.
A typical pattern is cloning just the parts of the import document you need. Given a source file and a page that wants one section from it:
<div class="warning">
<style>
h3 {
color: red !important;
}
</style>
<h3>Warning!
<p>This page is under construction
</div>
<div class="outdated">
<h3>Heads up!
<p>This content may be out of date
</div>
You can select elements within the import document and clone them into the main page:
<head>
<link rel="import" href="warnings.html">
</head>
<body>
...
<script>
var link = document.querySelector('link[rel="import"]');
var content = link.import;
// Grab DOM from warning.html's document.
var el = content.querySelector('.warning');
document.body.appendChild(el.cloneNode(true));
</script>
</body>
Script behavior inside imports
Scripts in an import run within the context of the importing page's window, so window.document refers to the main document. This means functions defined in an import become available globally, and there's no extra step to expose them—the scripts simply execute. This allows an import to wire its own stylesheets into the host page by inspecting its own document via document.currentScript.ownerDocument:
<link rel="stylesheet" href="http://www.example.com/styles.css">
<link rel="stylesheet" href="http://www.example.com/styles2.css">
<style>
/* Note: <style> in an import apply to the main
document by default. That is, style tags don't need to be
explicitly added to the main document. */
#somecontainer {
color: blue;
}
</style>
...
<script>
// importDoc references this import's document
var importDoc = document.currentScript.ownerDocument;
// mainDoc references the main document (the page that's importing us)
var mainDoc = document;
// Grab the first stylesheet from this import, clone it,
// and append it to the importing document.
var styles = importDoc.querySelector('link[rel="stylesheet"]');
mainDoc.head.appendChild(styles.cloneNode(true));
</script>
Two ordering rules are important: imports don't block parsing of the main page, but scripts within an import execute in document order, giving you defer-style behavior with proper sequencing.
Imports for Web Components
Imports are a natural distribution channel for Web Components. They carry the templates, custom element definitions, and shadow DOM logic a component needs in one request.
Templates and custom elements
Combining imports with the <template> element yields inert markup that only activates when the importer uses it—an import can define ready-to-use scaffolding:
import.html
<template>
<h1>Hello World!</h1>
<!-- Img is not requested until the <template> goes live. -->
<img src="world.png">
<script>alert("Executed when the template is activated.");</script>
</template>
The importer selects the template and stamps it into the page:
index.html
<head>
<link rel="import" href="import.html">
</head>
<body>
<div id="container"></div>
<script>
var link = document.querySelector('link[rel="import"]');
// Clone the <template> in the import.
var template = link.import.querySelector('template');
var clone = document.importNode(template.content, true);
document.querySelector('#container').appendChild(clone);
</script>
</body>
Custom elements fit particularly well here. Because imports execute scripts, they can register element definitions automatically. The consumer just uses the markup:
elements.html
<script>
// Define and register <say-hi>.
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.innerHTML = 'Hello, <b>' +
(this.getAttribute('name') || '?') + '</b>';
};
document.registerElement('say-hi', {prototype: proto});
</script>
<template id="t">
<style>
::content > * {
color: red;
}
</style>
<span>I'm a shadow-element using Shadow DOM!</span>
<content></content>
</template>
<script>
(function() {
var importDoc = document.currentScript.ownerDocument; // importee
// Define and register <shadow-element>
// that uses Shadow DOM and a template.
var proto2 = Object.create(HTMLElement.prototype);
proto2.createdCallback = function() {
// get template in import
var template = importDoc.querySelector('#t');
// import template into
var clone = document.importNode(template.content, true);
var root = this.createShadowRoot();
root.appendChild(clone);
};
document.registerElement('shadow-element', {prototype: proto2});
})();
</script>
The importing page works with these elements with no wiring code:
index.html
<head>
<link rel="import" href="elements.html">
</head>
<body>
<say-hi name="Eric"></say-hi>
<shadow-element>
<div>( I'm in the light dom )</div>
</shadow-element>
</body>
Sub-imports and dependency deduplication
Imports can include other imports to build on shared components. A Polymer example, a tabs component that depends on a selector and layout helpers, manages those dependencies purely through sub-imports:
<link rel="import" href="iron-selector.html">
<link rel="import" href="classes/iron-flex-layout.html">
<dom-module id="paper-tabs">
<template>
<style>...</style>
<iron-selector class="layout horizonta center">
<content select="*"></content>
</iron-selector>
</template>
<script>...</script>
</dom-module>
An application consuming the tabs component includes one file:
<link rel="import" href="paper-tabs.html">
<paper-tabs></paper-tabs>
If the underlying selector is replaced with a better version in the future, only the import document needs to change—consumers are unaffected.
The same behavior handles shared libraries gracefully. Wrap a library in an import:
jquery.html
<script src="http://cdn.com/jquery.js"></script>
Subsequent imports as well as the main page itself can pull it in repeatedly:
import2.html
<link rel="import" href="jquery.html">
<div>Hello, I'm import 2</div>
ajax-element.html
<link rel="import" href="jquery.html">
<link rel="import" href="import2.html">
<script>
var proto = Object.create(HTMLElement.prototype);
proto.makeRequest = function(url, done) {
return $.ajax(url).done(function() {
done();
});
};
document.registerElement('ajax-element', {prototype: proto});
</script>
<head>
<link rel="import" href="jquery.html">
<link rel="import" href="ajax-element.html">
</head>
<body>
...
<script>
$(document).ready(function() {
var el = document.createElement('ajax-element');
el.makeRequest('http://example.com');
});
</script>
</body>
Even with the same import referenced throughout the import graph, the browser fetches and evaluates it only once. The network requests confirm the deduplication:
Performance and loading behavior
HTML Imports add a new way to fetch resources, but the usual performance rules still apply. Minimizing network requests matters as much as ever. If a page has several top-level imports, it can be better to combine them into one file and import that single resource. The Polymer team's Vulcanize build tool does this automatically: it recursively flattens a set of HTML Imports into one file, acting as a concatenation step for Web Components.
It is also worth remembering that imports sit on top of the browser's ordinary networking stack. Sub-resources inside an import are cached normally, just like any other asset fetched over HTTP.
Imports are inert until used
An import's document is not automatically rendered or injected into the page. Only <script> inside an import executes immediately. Stylesheets and markup stay dormant until the importing page explicitly appends them to the DOM. This is the same behavior you get with a dynamically created stylesheet or element link: the request is not made, or the content is not applied, until the node is inserted into the document.
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
The browser will not request styles.css until the link is added to the DOM:
document.head.appendChild(link); // browser requests styles.css
The same applies to dynamically created markup — an h2 is meaningless until it is attached to the document:
var h2 = document.createElement('h2');
h2.textContent = 'Booyah!';
An import link is therefore not an "#include" directive that says "render content here." It tells the parser to fetch a document for later use. This differs from an <iframe>, which loads and renders content immediately. One exception: <style> elements do not need to be added explicitly.
Rendering and parsing behavior
Imports block rendering of the main page, much like <link rel="stylesheet">. This prevents a flash of unstyled content (FOUC), since imports may contain stylesheets. Imports do not, however, block parsing of the main page. Scripts inside imports run in order without blocking the importing page, giving defer-like behavior with correct ordering.
If you need the import to be fully asynchronous, add the async attribute:
<link rel="import" href="https://web.dev/path/to/import_that_takes_5secs.html" async>
async is not the default because synchronous loading guarantees that custom element definitions within imports are upgraded in order. With async imports, developers would have to manage upgrade timing themselves. You can also create an async import dynamically:
var l = document.createElement('link');
l.rel = 'import';
l.href = 'elements.html';
l.setAttribute('async', '');
l.onload = function(e) { ... };
A critical caveat: while imports themselves do not block parsing, a <script> in the main document still does. The first script following an import will block page rendering, because the import may contain script that must execute first:
<head>
<link rel="import" href="https://web.dev/path/to/import_that_takes_5secs.html">
<script>console.log('I block page rendering');</script>
</head>
How you handle this depends on your app's structure.
Scenario #1 (preferred): no script in <head> or inlined in <body>
Do not place <script> directly after your imports. Move scripts as late as possible in the page. Here, everything sits at the bottom:
<head>
<link rel="import" href="https://web.dev/path/to/import.html">
<link rel="import" href="https://web.dev/path/to/import2.html">
<!-- avoid including script -->
</head>
<body>
<!-- avoid including script -->
<div id="container"></div>
<!-- avoid including script -->
...
<script>
// Other scripts n' stuff.
// Bring in the import content.
var link = document.querySelector('link[rel="import"]');
var post = link.import.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
</script>
</body>
Scenario 1.5: the import adds itself
The import author can establish a contract so that the import appends its own content to a specific area of the main page:
import.html:
<div id="blog-post">...</div>
<script>
var me = document.currentScript.ownerDocument;
var post = me.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
</script>
index.html
<head>
<link rel="import" href="https://web.dev/path/to/import.html">
</head>
<body>
<!-- no need for script. the import takes care of things -->
</body>
Scenario #2: script must live in <head> or be inlined in <body>
If a slow import precedes a page script, that script will block rendering. For cases where you cannot avoid script in the head (e.g., Google Analytics tracking code), add the import dynamically to prevent blocking:
<head>
<script>
function addImportLink(url) {
var link = document.createElement('link');
link.rel = 'import';
link.href = url;
link.onload = function(e) {
var post = this.import.querySelector('#blog-post');
var container = document.querySelector('#container');
container.appendChild(post.cloneNode(true));
};
document.head.appendChild(link);
}
addImportLink('/path/to/import.html'); // Import is added early :)
</script>
<script>
// other scripts
</script>
</head>
<body>
<div id="container"></div>
...
</body>
Alternatively, place the import near the end of the <body>:
<head>
<script>
// other scripts
</script>
</head>
<body>
<div id="container"></div>
...
<script>
function addImportLink(url) { ... }
addImportLink('/path/to/import.html'); // Import is added very late :(
</script>
</body>
Key points to remember
- The import mimetype is
text/html. - Cross-origin resources must be CORS-enabled.
- A given URL is fetched and parsed only once; its scripts run only the first time the import is encountered.
- Scripts in imports execute in order and do not block main document parsing.
Use cases and value
HTML Imports bundle HTML, CSS, and JavaScript into a single resource. That capability becomes especially useful with Web Components, letting developers ship reusable components that consumers bring into their own apps with <link rel="import">. The concept is simple but enables several useful patterns:
- Distribute related HTML/CSS/JS as a single resource — theoretically even an entire web app.
- Organize code by segmenting concepts into different files, supporting modularity and reusability.
- Deliver Custom Element definitions, keeping an element's interface separate from its usage.
- Manage dependencies — resources are automatically de-duplicated.
- Chunk scripts — a library can start executing as soon as its first parsed chunk arrives, reducing initial latency.
- Parallelize HTML parsing — the browser can now run multiple HTML parsers concurrently.
- Switch debug and production modes by changing only the import target, without the app knowing whether it is a compiled bundle or a tree of imports.



