Alokkumar Rathava

Articles / Engineering

Declarative HTML Includes: Building <html-include> and Proposing It to WHATWG

Reusing a header or footer across static HTML pages still needs a framework or build step. I built a zero-dependency <html-include> Web Component, took it to WHATWG, and here is why a native primitive is worth considering.

If you have built a multi-page website using plain HTML, CSS, and vanilla JavaScript, you have probably encountered a surprisingly persistent problem:

How do you reuse the same header, navigation bar, or footer across multiple HTML pages without copying it into every file?

Copy-pasting works initially, but it quickly becomes a maintenance problem. A single navigation change may require editing dozens of .html files. Miss one page, and the site becomes inconsistent.

Traditionally, developers solve this using:

  • Server-side includes
  • Template engines such as PHP, Django, or Express
  • Static site generators such as Eleventy, Astro, or Next.js
  • Client-side JavaScript frameworks
  • Build-time bundlers and component systems

These are effective solutions for larger applications. But what about a small static website hosted on GitHub Pages, Cloudflare Pages, Netlify, or a basic CDN?

Introducing Node.js, package management, build pipelines, and framework tooling simply to reuse a footer can feel like unnecessary complexity.

The underlying problem is that the browser still lacks a native, declarative mechanism for including reusable HTML fragments.

To explore what such a primitive could look like, I created a zero-dependency Web Component called <html-include> and submitted a feature proposal to the WHATWG HTML Living Standard as Issue #12747.

This article explains how <html-include> works, the browser-engine edge cases it addresses, and why declarative HTML inclusion deserves consideration as a native web-platform capability.

The Idea: Declarative Composition in Vanilla HTML

The core idea is simple: developers should be able to fetch and compose reusable HTML fragments directly from markup, without rewriting the same fetch() and DOM-manipulation logic for every project.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./html-include.js" defer></script>
</head>
<body>
  <!-- Declarative header fragment -->
  <html-include
    src="partials/header.html"
    min-height="65px"
    fade
  >
    <div class="skeleton">Loading navigation…</div>
  </html-include>
  <main>
    <h1>Welcome to my static site</h1>
  </main>
  <!-- Shared footer fragment -->
  <html-include
    src="partials/footer.html"
    min-height="80px"
  ></html-include>
</body>
</html>

The element's initial light-DOM children act as fallback content while the request is loading.

They can also provide meaningful text when JavaScript is unavailable or when the fragment cannot be loaded.

Once the request succeeds, the fetched fragment replaces the fallback markup.

The result is a small, declarative composition model that works with ordinary HTML files and requires no framework or build process.

Why This Is Harder Than Calling fetch()

At first glance, an HTML include element appears easy to implement:

  • Read the src attribute.
  • Fetch the file.
  • Insert the response into the DOM.

A basic implementation can be written in a few lines of JavaScript.

A production-ready implementation is much more difficult.

Once HTML fragments interact with browser parsing rules, network timing, nested inclusions, scripts, accessibility, and layout rendering, several architectural problems appear.

The following sections describe the major edge cases addressed by <html-include>.

1. Preventing Cumulative Layout Shift and FOUC

Unknown custom elements are rendered inline by default. If an include element begins with little or no height and then receives a large fragment from the network, the browser must push the surrounding page content downward.

This causes Cumulative Layout Shift, or CLS.

For example, imagine that a page begins rendering before its navigation fragment arrives. When the header finishes loading, the entire page suddenly moves.

To reduce that problem, <html-include> applies block-level layout behavior and supports geometry-reservation attributes such as min-height and aspect-ratio.

<html-include
  src="partials/hero.html"
  aspect-ratio="16 / 9">
  <div class="skeleton">Loading featured content…</div>
</html-include>

The browser can reserve the expected space before the network request finishes.

This allows developers to:

  • Avoid sudden layout movement
  • Display skeleton loading states
  • Prevent flashes of unstyled or missing content
  • Preserve predictable page geometry

A native implementation could improve this further by integrating the element directly into browser layout and preload behavior.

2. The HTML Parser's Foster-Parenting Problem

Custom elements cannot safely appear in every HTML context.

HTML parsing is not based only on DOM validity after the page loads. The parser applies special insertion rules while converting source markup into a document tree.

Consider a custom element placed inside a table:

<table>
  <html-include src="partials/table-rows.html"></html-include>
</table>

This does not behave as developers may expect.

Because <html-include> is not valid table content, the HTML parser may move it outside the table before the custom element's JavaScript is executed. This behavior is part of the parser's foster-parenting rules.

Similar restrictions exist in tightly controlled structures such as:

  • <table>
  • <tbody>
  • <tr>
  • <ul>
  • <ol>
  • <select>

A Web Component cannot override decisions the HTML parser has already made.

To address this limitation, the implementation supports an attribute-based inclusion mode attached to valid native elements.

<table class="data-table">
  <tbody data-include="partials/table-rows.html"></tbody>
</table>

Here, <tbody> remains valid inside the table, so the parser does not relocate it.

The implementation can then fetch the fragment and insert the table rows into the correct host element.

This provides two complementary authoring modes:

<html-include src="partials/content.html"></html-include>

and:

<tbody data-include="partials/table-rows.html"></tbody>

The second form is particularly important because a native inclusion proposal must account for the HTML parser's content models rather than assuming a new element can be placed everywhere.

3. Detecting Circular Includes

Nested fragments are useful.

A page may include a header, and the header may include a navigation fragment:

index.html
└── header.html
    └── navigation.html

However, nested includes introduce the possibility of circular dependencies.

For example:

header.html
└── navigation.html
    └── header.html
        └── navigation.html
            └── ...

Without protection, this produces an endless sequence of network requests and DOM insertions.

<html-include> prevents this by tracking the inclusion ancestry of each fragment.

Before loading a new resource, the element:

  • Resolves the fragment to an absolute URL.
  • Walks through its containing include elements.
  • Checks whether the same URL already exists in the ancestor chain.
  • Stops the request if a cycle is detected.
  • Enforces a configurable maximum nesting depth.

Conceptually, the result looks like this:

header.html
└── navigation.html
    └── header.html
        └── Blocked: circular inclusion detected

The ancestry traversal also accounts for Shadow DOM boundaries, where a simple parentElement loop may not be sufficient.

Cycle detection is essential for both reliability and debuggability. A native browser feature would require similarly well-defined behavior for recursive inclusions.

4. Script Security and Controlled Execution

Fetching HTML and assigning it to innerHTML can introduce scripts into the document.

That creates several questions:

  • Should scripts execute automatically?
  • Should inline scripts be allowed?
  • Should external scripts be loaded?
  • How should Content Security Policy apply?
  • What happens when the same partial is included more than once?
  • Should scripts execute in document order?

The safest default is to treat included fragments as markup rather than executable application code.

For that reason, <html-include> removes <script> elements by default before inserting a fragment into the page.

<html-include src="partials/article-card.html"></html-include>

Any scripts inside article-card.html are stripped.

For trusted first-party fragments, execution can be explicitly enabled:

<html-include
  src="partials/trusted-widget.html"
  allow-scripts
></html-include>

When script execution is enabled, scripts cannot simply be inserted using innerHTML. Script elements created through HTML parsing are generally inert when inserted this way.

The implementation therefore recreates allowed scripts as fresh DOM nodes and processes them sequentially.

This makes it possible to preserve execution order while continuing to respect the document's Content Security Policy, including applicable script nonces.

The design follows an important security principle:

Loading markup should be the default. Executing code should require explicit developer intent.

A native inclusion primitive would need a carefully defined script-processing model rather than inheriting ambiguous behavior from ordinary DOM insertion.

5. Handling Network Races and Request Lifecycles

An include element may change dynamically.

const include = document.querySelector("html-include");
include.src = "partials/profile-a.html";
include.src = "partials/profile-b.html";

Suppose the first request is slow and the second is fast.

Without lifecycle protection, the following sequence may occur:

  • profile-a.html begins loading.
  • profile-b.html begins loading.
  • profile-b.html finishes and is displayed.
  • profile-a.html finishes later.
  • The stale response overwrites the current content.

This is a classic asynchronous race condition.

<html-include> uses AbortController to associate each load operation with its own request lifecycle.

When the src attribute changes, the element reconnects, or .load() is called again:

  • The previous request is aborted.
  • Its response is ignored.
  • Only the latest active request may update the DOM.

This makes dynamic fragment loading predictable and prevents stale network responses from replacing newer content.

6. Resolving Nested Relative URLs

Included fragments may contain links, images, stylesheets, or additional includes using relative paths.

For example, suppose the page loads:

/pages/index.html

and includes:

/components/header/header.html

Inside header.html, an image may be referenced as:

<img src="./logo.svg" alt="Company logo" />

The browser normally resolves that path relative to the containing document, not relative to the fetched fragment.

That may incorrectly resolve to:

/pages/logo.svg

instead of:

/components/header/logo.svg

A fragment inclusion system therefore needs a clear base-URL model.

The implementation resolves relevant fragment URLs against the fetched partial's location before insertion.

This applies to resources such as:

  • Nested include URLs
  • Images
  • Links
  • Stylesheets
  • Media sources
  • Form actions

Without predictable URL resolution, reusable fragments become dependent on the directory of every page that includes them.

A native primitive could provide a standardized fragment base URL in the same way browsers already define URL-resolution behavior for documents, modules, and stylesheets.

7. Loading, Success, and Error States

Network operations fail.

A fragment may be missing, the server may return an error, the user may be offline, or the request may be blocked by cross-origin policy.

A reusable include primitive therefore needs observable lifecycle states.

The component exposes state through attributes and custom events so applications can react without modifying its internal implementation.

Conceptually, the element moves through states such as:

idle → loading → loaded
               └→ error

Developers can listen for events:

const include = document.querySelector("html-include");
include.addEventListener("include-load", event => {
  console.log("Fragment loaded", event.detail);
});
include.addEventListener("include-error", event => {
  console.error("Fragment failed", event.detail);
});

They can also style the element based on its current state:

html-include[loading] {
  opacity: 0.7;
}
html-include[error] {
  border: 1px solid currentColor;
}

The original fallback content can remain visible when loading fails, ensuring the page does not silently collapse into an empty region.

8. Avoiding Duplicate Work

The same fragment may appear multiple times on a page.

Without caching, every instance could produce another network request for identical content.

Browsers already provide HTTP caching, but an inclusion component can still reduce unnecessary parsing and repeated work during the same document lifecycle.

A useful implementation may cache successfully fetched fragment text by normalized URL while ensuring that every inclusion still receives its own DOM nodes.

The cache stores the response source, not a single reused DOM subtree.

This distinction matters because one DOM node cannot exist in multiple locations simultaneously. Moving an existing node to a new parent would remove it from the previous include.

A native implementation could potentially integrate more efficiently with the browser's resource cache and HTML parser.

The Userland API

The reference implementation is intentionally small and HTML-oriented.

A basic include requires only a source:

<html-include src="partials/header.html"></html-include>

Fallback markup can be placed inside the element:

<html-include src="partials/navigation.html">
  <nav aria-label="Primary navigation">
    Navigation is temporarily unavailable.
  </nav>
</html-include>

Layout space can be reserved:

<html-include
  src="partials/banner.html"
  min-height="120px"></html-include>

An aspect ratio can be declared:

<html-include
  src="partials/video-card.html"
  aspect-ratio="16 / 9"
></html-include>

Trusted scripts can be enabled explicitly:

<html-include
  src="partials/trusted-widget.html"
  allow-scripts
></html-include>

Native host elements can be used in parser-restricted contexts:

<select>
  <option value="">Select a country</option>
  <optgroup
    label="Available countries"
    data-include="partials/country-options.html"
  ></optgroup>
</select>

Fragments can also be reloaded programmatically:

document
  .querySelector("html-include")
  .load();

The goal is not to recreate a full component framework. It is to provide one focused capability: declarative HTML fragment composition.

Why Not Just Use JavaScript?

You absolutely can implement HTML inclusions with JavaScript.

In fact, <html-include> itself is currently implemented in JavaScript.

The question is not whether userland code can perform the task. The question is whether every developer should have to independently reimplement the same browser-level behavior.

A typical project must otherwise decide how to handle:

  • Fetching
  • Request cancellation
  • Loading states
  • Error states
  • Fallback content
  • Relative URL resolution
  • Nested includes
  • Circular dependencies
  • Script filtering
  • CSP compatibility
  • Parser-restricted contexts
  • DOM insertion
  • Layout reservation
  • Accessibility behavior

When the same infrastructure appears repeatedly across unrelated projects, it may indicate a missing platform primitive.

Developers once implemented lazy loading, responsive image selection, dialogs, disclosure widgets, and module loading through custom JavaScript. Many of those capabilities eventually gained native browser support.

Declarative HTML composition may deserve similar consideration.

Why Not Use <iframe>?

An <iframe> can load another HTML document, but it is not an HTML partial inclusion mechanism.

Frames create separate browsing contexts with their own:

  • Document
  • Window
  • JavaScript environment
  • CSS scope
  • Accessibility tree boundary
  • Navigation lifecycle
  • Security model

A shared site header loaded through an iframe would not behave like part of the surrounding document.

It would also introduce complications for:

  • Responsive layout
  • Height synchronization
  • Keyboard navigation
  • Styling
  • Forms
  • Event handling
  • Search indexing
  • Printing
  • Accessibility

An include primitive should compose markup into the current document rather than embedding an independent webpage.

Why Not Use Web Components Alone?

Web Components are powerful, but they solve a broader and somewhat different problem.

A custom element can encapsulate rendering logic, state, styles, and behavior. However, loading an ordinary HTML fragment still requires developers to write a JavaScript class that handles the entire request and insertion lifecycle.

For many static websites, the desired abstraction is much smaller:

<html-include src="footer.html"></html-include>

No component class. No package installation. No framework runtime. No build system.

A native inclusion element could coexist with Web Components rather than replace them.

It could even be used inside a custom element when a component needs to compose external declarative markup.

Why a Native Browser Primitive Would Be Better

The userland implementation demonstrates that declarative inclusion is practical, but browser-native support could offer capabilities that JavaScript cannot fully reproduce.

Zero JavaScript Runtime

A native element could work before application scripts are downloaded, parsed, or executed.

Static websites could use reusable fragments without shipping a JavaScript loader.

Preload Scanner Integration

Browser preload scanners discover resources such as scripts, stylesheets, images, and modules while the main parser continues processing the document.

A native include element could allow fragment URLs to be discovered early:

<html-include src="/shared/navigation.html"></html-include>

The browser could begin fetching the fragment immediately rather than waiting for a custom-element definition to execute.

Streaming HTML Parsing

A JavaScript implementation normally downloads the fragment as text and then parses it.

A browser-native implementation could potentially stream the response directly into an appropriate HTML parsing context.

This may improve both performance and memory usage.

Correct Parser Context

Native integration could define how fragments are parsed inside tables, lists, selects, templates, and other special content models.

Userland custom elements cannot override parser behavior that occurs before JavaScript executes.

Standardized Security Behavior

A specification could clearly define:

  • Whether scripts execute
  • Whether inline event handlers are allowed
  • How CSP applies
  • How cross-origin fragments behave
  • Whether credentials are included
  • How referrer policies apply
  • How nested includes inherit permissions

Better Developer Tools

Browsers could expose fragment loading in:

  • The Network panel
  • The Elements panel
  • Performance traces
  • Accessibility inspection
  • Error messages
  • Resource timing APIs

Search and Accessibility Integration

Because the content would be part of the document's actual DOM rather than a separate browsing context, browsers and assistive technologies could treat it as ordinary page content.

Exact indexing and rendering behavior would still need careful specification, especially for crawlers that do not execute dynamic loading.

What a Standard Would Need to Define

The syntax is the easy part.

A serious standards proposal must define processing behavior in detail.

Questions include:

Fetching

  • Are requests made using CORS?
  • Are credentials included?
  • Which referrer policy applies?
  • Can developers configure request priority?
  • Can fragments be preloaded?

Parsing

  • Is the fragment parsed using the host element's context?
  • How are document-level tags such as <html>, <head>, and <body> handled?
  • What happens with malformed markup?
  • How do parser-restricted contexts work?

Scripts

  • Are scripts blocked by default?
  • Can they be enabled?
  • Do module scripts behave differently?
  • How is execution order defined?
  • What happens when a cached fragment is included multiple times?

Styles

  • Do fragment styles apply to the entire document?
  • Can styles be scoped to the included subtree?
  • Should declarative Shadow DOM be supported?
  • How are stylesheet URLs resolved?

Nesting

  • Are nested includes allowed?
  • How are circular dependencies detected?
  • Is there a maximum nesting depth?

Lifecycle

  • Which events are emitted?
  • What fallback content remains on error?
  • What happens when src changes?
  • When does the element count as loaded?
  • Should fragment requests affect browser history?
  • Should links inside fragments behave exactly like normal links?
  • How do base URLs work?

These questions are why the proposal should be incubated carefully rather than treated as merely a shorthand for fetch() plus innerHTML.

Taking the Idea to WHATWG

To move the discussion beyond a userland experiment, I opened WHATWG HTML Issue #12747, proposing the incubation of a native declarative HTML inclusion mechanism such as:

<html-include src="/shared/header.html"></html-include>

or potentially:

<include src="/shared/header.html"></include>

The purpose of the proposal is not to insist that the reference implementation's API must become the final standard.

Instead, the project demonstrates:

  • Clear developer demand
  • A practical declarative authoring model
  • Real browser-parser constraints
  • Security considerations
  • Lifecycle requirements
  • Potential native performance benefits

The next step is discussion among browser engineers, standards editors, accessibility experts, security reviewers, and developers.

Try the Reference Implementation

You do not need to wait for a browser standard to experiment with declarative HTML partials.

  • Live demo: alokrathava.github.io/html-include
  • GitHub repository: github.com/alokrathava/html-include
  • WHATWG proposal: HTML Issue #12747

The project is zero-dependency and designed for static websites, progressive enhancement, and straightforward integration with ordinary HTML.

Final Thoughts

The web platform is exceptionally capable, but composing reusable HTML across static pages still requires more infrastructure than the problem seems to justify.

For a large application, a framework, server template system, or static site generator may be the right answer.

For a small website, developers should also have a smaller option.

A native declarative inclusion primitive could provide:

  • Reusable HTML fragments
  • No required build step
  • No framework dependency
  • No custom fetch boilerplate
  • Predictable loading and error behavior
  • Browser-level parsing and performance optimizations
  • A standardized security model

<html-include> is an attempt to explore that missing layer.

The userland implementation is available today. The broader question is whether fragment inclusion should remain something every developer rebuilds independently - or become a capability provided directly by the browser.

If you believe declarative HTML partials belong on the web platform, test the implementation, review the source code, and contribute your perspective to the WHATWG discussion.