# include-content

One-stop shop for rendering a view when & where you need it — lazy-loading/unloading, conditional rendering, portalling… no app JS required.


```html
<div>
  <a>Hover me</a>
  <include-content lazy-load>
    <template>
      <p>HTML inserted</p>
    </template>
  </include-content>
  <small role="note">See Live document tab for DOM changes.</small>
  <style>
    #demo-include-content-simple > :first-child {
      a:not(:hover) + include-content:not([is-active]) {
        display: none;
      }
    }
  </style>
</div>
```


## Features

- **Zero JS** Sophisticated UX from a simple HTML-only API, as with all NucleusKit elements.
- **Lazy (un)load** Lazy load and lazy unload your views
- **Eager / idle** Prioritize critical content; defer the rest
- **Prefetch** Warm templates so they're ready on activate
- **Shared / remote templates** Point at a DOM `<template>` or URL
- **Choose the host** Portal into light DOM, shadow, author iframe, or any selector
- **Keep state** Reuse the same tree across toggles
- **Animatable** Built-in fade, or bring your own with `.instant`

## Installation


`@excom/include-content` v0.1.4

```bash
pnpm add @excom/include-content
```

```bash
npm install @excom/include-content
```

```bash
yarn add @excom/include-content
```

### Import

```ts
import "@excom/include-content";
```



## Usage

Put a `<template>` inside (or set `template-ref`) and pick when it should appear. For conditional rendering, use Quark or JS to toggle `is-active`.

```html
<include-content lazy-load template-ref="/path/to/view.html"></include-content>
```

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description | Inherited from |
| --- | --- | --- | --- | --- | --- | --- |
| `idle-load` | option | `boolean` |  |  | Activate after first paint, when the browser is idle. Useful for below-the-fold / secondary views that should not compete with critical content. |  |
| `lazy-load` | option | `boolean` |  |  | Activate when this element is on screen. Pair with `lazy-unload` for lazy load and lazy unload of views. |  |
| `lazy-unload` | option | `boolean` |  |  | Deactivate (unrender) when no longer on screen. Use with `lazy-load` to free DOM for off-screen views. |  |
| `observer-root` | option | `string` |  | `<CSS Selector>` | Scroll container for lazy load / unload. Defaults to the viewport. |  |
| `observer-root-margin` | option | `string` | `"-1px -1px -1px -1px"` | `<length>` \| `<percentage>` | How far outside the root counts as "visible". Expand (e.g. `500px`) to lazy-load a view *before* it enters the viewport so users never see an empty slot; shrink (default `-1px`) so edge-flush elements wait until they truly enter. |  |
| `observer-threshold` | option | `number` |  |  | Fraction of the element that must be visible (`0`–`1`) before activating. |  |
| `observer-delay` | option | `number` | `0` |  | Minimum time visible before activating (ms). Ignored where unsupported. Useful for ensuring lazy load is not triggered when a programmatic smooth scroll zips the user right past the element. |  |
| `template-ref` | option | `string` | `":scope > template"` | `<CSS Selector>` \| `<URL>` | Source `<template>` — in-document selector or remote URL. Changing mid-flight aborts and reloads. Can use `:scope` to relatively select elements: e.g. `main:has(:scope) > template` | `@excom/renderable-element` |
| `bypass-cache` | option | `boolean` |  |  | Skip the in-memory response cache (URL `template-ref` only). | `@excom/renderable-element` |
| `pre-fetch` | option | `string` | `"lazy"` | `""` \| `"eager"` \| `"idle"` \| `"lazy"` | When to fetch the template, independent of when it renders. `""` aliases `eager`. `idle` never runs in a prerender. | `@excom/renderable-element` |
| `persist-content` | option | `boolean` |  |  | Reuse the same live nodes across unrender / render (held on `_persistedTree`) so form values, scroll position, and subtree state survive toggles. | `@excom/renderable-element` |
| `host-ref` | option | `string` |  | `"shadow"` \| `"iframe"` \| `<CSS Selector>` | Where rendered children land. Unset = this element's light DOM. `shadow` attaches an open shadow root. `iframe` paints into a child `<iframe data-render-host>` body (you supply the iframe — useful for sandboxed / third-party document isolation). Any other value is a portal selector. | `@excom/renderable-element` |
| `ready-on` | option | `string` |  | `<Event Name>` | Event name that marks rendered children "ready". Until it fires, `delaying-ready` is set so CSS can hide the host for a coordinated paint / view transition. Prerendered content kept at hydration is ready at once, never hidden. | `@excom/renderable-element` |
| `is-active` | hybrid | `boolean` |  |  | Master switch. Set to load (if needed) and render; unset to unrender. Drive from visibility, route match, hover, etc. | `@excom/renderable-element` |
| `is-loading` | state | `boolean` |  |  | Template fetch in flight. | `@excom/renderable-element` |
| `did-load` | state | `boolean` |  |  | Template resolved at least once. Stays set across `is-active` toggles so consumers know later paints are warm (URL refs reuse the shared fetch cache in kit-utils). Cleared when `template-ref` changes or `--reload` forces a fresh resolve. | `@excom/renderable-element` |
| `is-error` | state | `boolean` |  |  | Latest template fetch rejected (excluding abort). Fires with the `error` event. | `@excom/renderable-element` |
| `delaying-ready` | state | `boolean` |  |  | Between `render` and the matching `ready-on` event (or a failed load). Hook with CSS for coordinated paints / view transitions. | `@excom/renderable-element` |

#### Recognized Elements

| Relationship | Selector | Required | Description | Inherited from |
| --- | --- | --- | --- | --- |
| `template` | child | no | Optional immediate `<template>` child used when `template-ref` is the default `":scope > template"`. Not required when `template-ref` points at a selector or URL elsewhere. | `@excom/renderable-element` |
| `iframe[data-render-host]` | child | no | Required when `host-ref="iframe"`. Content paints into `iframe.contentDocument.body`. Provide your own iframe (e.g. with `srcdoc`); the element will not create one. | `@excom/renderable-element` |

#### Fires

| Name | Type | Description | Inherited from |
| --- | --- | --- | --- |
| `include-content-render` | `RenderableRenderEvent` (`CustomEvent & { type: "{tag}-render"; detail: () => Promise<void>; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes active and is about to place template content into the host. `event.detail` is a thunk that performs the load (if not already loaded) and renders the children, returning a Promise that resolves once the corresponding `ready-on` event fires (or immediately if `ready-on` is unset). The promise rejects if the template fails to load, `host-ref` resolves to no host (nor an author iframe still loading), or the element is torn down mid-flight (`startTeardown` while loading / `delaying-ready`). Call `preventDefault()` to defer rendering and invoke `event.detail()` later. | `@excom/renderable-element` |
| `include-content-unrender` | `RenderableUnrenderEvent` (`CustomEvent & { type: "{tag}-unrender"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes inactive and content is already painted. `event.detail` is a thunk that removes the rendered children. Call `preventDefault()` to defer the removal. Not fired when teardown cancels an in-flight load — that path emits `aborted` instead. | `@excom/renderable-element` |
| `include-content-did-render` | `RenderableDidRenderEvent` (`CustomEvent & { type: "{tag}-did-render"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after the template content has actually been placed into the host. | `@excom/renderable-element` |
| `include-content-did-unrender` | `RenderableDidUnrenderEvent` (`CustomEvent & { type: "{tag}-did-unrender"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after rendered children have been removed from the host. | `@excom/renderable-element` |
| `include-content-error` | `RenderableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the template promise rejects with anything other than an `AbortError`. | `@excom/renderable-element` |
| `include-content-aborted` | `RenderableAbortedEvent` (`CustomEvent & { type: "{tag}-aborted"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when an in-flight load / ready wait is canceled because `is-active` was unset (via `startTeardown`). | `@excom/renderable-element` |

#### Commands

| Command | Action | Inherited from |
| --- | --- | --- |
| `--reload` | Stops waiting for any in-flight load and re-resolves the template, bypassing the cache for URL refs (useful after remote content changes). A URL request is shared by the page, so the one in flight is not cancelled. | `@excom/renderable-element` |

#### Default actions

| Event | Default behavior (unless preventDefault() is called) | Inherited from |
| --- | --- | --- |
| `include-content-render` | Invokes `event.detail()` to load (if needed) and render the template into the host. | `@excom/renderable-element` |
| `include-content-unrender` | Invokes `event.detail()` to remove rendered children from the host. | `@excom/renderable-element` |

#### CSS Custom Properties

| Name | Syntax | Default | Description |
| --- | --- | --- | --- |
| `--include-content-transition-duration` | `<time>` | `0.15s` | Fade-in duration. |
| `--include-content-transition-ease` | `*` | `ease-in` | Fade-in easing. |

#### CSS Classes

| Name | Description |
| --- | --- |
| `.instant` | Skip the built-in fade — bring your own animation / view transition. Also opts-out if the element is portalling content elsewhere ([host-ref]) |

#### CSS Aliases

| Alias | Kind | Matches | Description |
| --- | --- | --- | --- |
| `:--include-content` | element | `include-content`, `.tag-include-content` |  |
| `:--include-content--lazy-load` | state | `[lazy-load]`, `[data-lazy-load]` |  |
| `:--include-content--lazy-unload` | state | `[lazy-unload]`, `[data-lazy-unload]` |  |
| `:--include-content--did-load` | state | `[did-load]`, `[data-did-load]` |  |
| `:--include-content--is-active` | state | `[is-active]`, `[aria-current]` |  |
| `:--include-content--host-shadow` | state | `[host-ref="shadow"]`, `[data-host-ref="shadow"]` |  |



### Examples

#### Template include

Break your app into smaller views. Point `template-ref` at a `<template>` or a URL with `is-active` to render it immediately. This demo does both. A `<script>` inside a URL template does not run; stylesheets, `<style>` and `<quark-sheet>` do.


```html
<div>
  <template id="ic-include-piece">
    <p>A reusable view piece.</p>
  </template>
  <include-content is-active template-ref="#ic-include-piece"></include-content>
  <include-content is-active template-ref="/fragments/remote-include.html"></include-content>
</div>
```


#### Lazy Load & Unload

`lazy-load` waits until on screen; `lazy-unload` removes it when it leaves. Give the host a real `min-height` so the trigger isn't ambiguous. Tune with `observer-root`, `observer-root-margin`, `observer-threshold`, and `observer-delay` — e.g. expand the margin to pre-render before the user scrolls to it. If remote template, pair with `pre-fetch="idle"` to warm the cache early.


```html
<section>
  <p>Scroll down…</p>
  <include-content lazy-load lazy-unload observer-root="section:has(> :scope)"
    observer-root-margin="0px" observer-threshold="0.5">
    <template>
      <p>Rendered at 50% visibility. Scroll away to remove
        (<code>lazy-unload</code>).</p>
    </template>
  </include-content>
  <p>Keep scrolling…</p>
  <style>
    #demo-include-content-lazy > :first-child {
      include-content {
        /* Prevent size collapse → render/unrender loop */
        min-height: 108px;
      }
    }
  </style>
</section>
```


#### Portal elsewhere

By default content lands in the element's light DOM. Set `host-ref` to
`shadow`, `iframe` (with a child `<iframe data-render-host>`), or any CSS
selector to render somewhere else. The iframe host only moves nodes: custom
elements inside it upgrade only if that document loads their definitions.


```html
<div>
  <include-content is-active host-ref="#ic-portal-mount">
    <template>
      <p>Portaled into the aside.</p>
    </template>
  </include-content>
  <aside id="ic-portal-mount"></aside>
</div>
```


#### Keep tree state

Toggling `is-active` off leaves `did-load` set (warm re-resolve; URL
refs hit the shared fetch cache). `persist-content` goes further and
reuses the same live nodes so implicit state (form values, open details,
etc.) survives toggles.


```html
<div>
  <quark-sheet>
    :scope {
      @on change {
        include-content { is-active: if(event.target.checked: ""; else: none); }
      }
    }
  </quark-sheet>
  <form>
    <label>
      <input type="checkbox" name="is-active" checked />
      Active
    </label>
  </form>
  <include-content is-active persist-content>
    <template>
      <label>
        Type, then toggle Active —
        <input type="text" placeholder="value is kept" />
      </label>
    </template>
  </include-content>
</div>
```

## Release notes

### 0.1.1 (2026-09-23)

- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources

## Demo sources

### template-ref

```html
<div>
  <template id="ic-shared-template">
    <p>Shared template — both hosts render this node.</p>
  </template>
  <include-content is-active template-ref="#ic-shared-template"></include-content>
  <include-content idle-load template-ref="#ic-shared-template"></include-content>
</div>
```
