npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@excom/renderable-element

v0.3.0

Published

RenderableElement base for Neutron elements

Readme

renderable-element

Composition base for Neutron elements that defer rendering a <template> until the right moment. It owns the load + render lifecycle so subclasses only decide when to flip is-active. Used by <include-content>, <spa-route>, and any custom element you compose yourself.

The demos below use <include-content> (the simplest concrete subclass) to exercise behavior that comes straight from this mixin.

<p>Type into both inputs, then toggle them off and on. The right side
keeps its value because <code>persist-content</code> reuses the same
template node instead of cloning a fresh copy each render.</p>

<style>
  .persist-demo {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
  .persist-demo include-content {
    display: block;
    border: 1px dashed currentColor;
    padding: 8px;
    margin-top: 4px;
  }
</style>

<div class="persist-demo">
  <div>
    <strong>Default (cloned each render):</strong>
    <label>
      <input type="checkbox" checked />
      Active
    </label>
    <include-content is-active>
      <template>
        <input type="text" placeholder="Type, then toggle off + on" />
      </template>
    </include-content>
  </div>

  <div>
    <strong><code>persist-content</code>:</strong>
    <label>
      <input type="checkbox" checked />
      Active
    </label>
    <include-content is-active persist-content>
      <template>
        <input type="text" placeholder="Type, then toggle off + on" />
      </template>
    </include-content>
  </div>

  <quark-sheet>
    label {
      @on change { + include-content { is-active: event.target.checked; } }
    }
  </quark-sheet>
</div>

Features

  • Template resolution via template-ref in-document selectors or remote URLs
  • Prefetch strategies lazy (default), eager, or idle
  • Configurable render host light DOM, shadow, author iframe, or any selector
  • Cancelable render / unrender parents can wrap updates in view transitions
  • Persistable content keep live subtree state across unrender / render cycles
  • Ready coordination ready-on + delaying-ready for paint-synced reveals

Installation

@excom/renderable-element v0.3.0

pnpm add @excom/renderable-element
npm install @excom/renderable-element
yarn add @excom/renderable-element

Import

import { /* … */ } from "@excom/renderable-element";

Usage

Compose RenderableElement into a Neutron class and toggle isActive from whatever signal makes sense — a media query, a websocket message, an experiment flag, etc. Everything else (template fetch, caching, host resolution, lifecycle events) is inherited.

import { Neutron } from "@excom/neutron";
import { RenderableElement } from "@excom/renderable-element";

export const MediaGated = Neutron.compose([
  RenderableElement,
  Neutron({
    tag: "media-gated",
    props: {
      mediaQuery: String,
    },
  }),
])
  .onPropChanged("mediaQuery", (el, prev) => {
    prev.mediaQuery && el._mql?.removeEventListener("change", el._sync);
    if (!el.mediaQuery) return { isActive: false };
    el._mql = window.matchMedia(el.mediaQuery);
    el._sync = () => (el.isActive = el._mql.matches);
    el._mql.addEventListener("change", el._sync);
    el._sync();
  });

MediaGated.define();
<media-gated media-query="(min-width: 900px)">
  <template>
    <wide-screen-only></wide-screen-only>
  </template>
</media-gated>

Event names are prefixed with the concrete tag (shown as {tag}-… in the API). For <include-content> that means include-content-render, etc.

API Reference

Attributes

| Name | Surface | Type | Default | Values | Description | | --- | --- | --- | --- | --- | --- | | 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 | | bypass-cache | option | boolean | | | Skip the in-memory response cache (URL template-ref only). | | 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. | | 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. | | 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. | | 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. | | is-active | hybrid | boolean | | | Master switch. Set to load (if needed) and render; unset to unrender. Drive from visibility, route match, hover, etc. | | is-loading | state | boolean | | | Template fetch in flight. | | 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. | | is-error | state | boolean | | | Latest template fetch rejected (excluding abort). Fires with the error event. | | delaying-ready | state | boolean | | | Between render and the matching ready-on event (or a failed load). Hook with CSS for coordinated paints / view transitions. |

Recognized Elements

| Relationship | Selector | Required | Description | | --- | --- | --- | --- | | 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. | | 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. |

Fires

| Name | Type | Description | | --- | --- | --- | | {tag}-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. | | {tag}-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. | | {tag}-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. | | {tag}-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. | | {tag}-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. | | {tag}-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). |

Commands

| Command | Action | | --- | --- | | --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. |

Default actions

| Event | Default behavior (unless preventDefault() is called) | | --- | --- | | {tag}-render | Invokes event.detail() to load (if needed) and render the template into the host. | | {tag}-unrender | Invokes event.detail() to remove rendered children from the host. |

Examples

Choosing a render host

Unset host-ref renders into the element's light DOM. "shadow" attaches an open shadow root for style isolation:

<p>Two <code>&lt;include-content&gt;</code> elements rendering the same template into different hosts. The shadow-root version is style-isolated.</p>

<style>
  .host-demo include-content {
    display: block;
    border: 1px dashed currentColor;
    padding: 8px;
    margin: 8px 0;
  }
  /* this rule cannot reach into a shadow root */
  .host-demo h4 { color: rebeccapurple; }
</style>

<div class="host-demo">
  <strong>Light DOM (default):</strong>
  <include-content is-active>
    <template>
      <h4>I'm rebeccapurple</h4>
      <p>Light-DOM children inherit the host page's CSS.</p>
    </template>
  </include-content>

  <strong>Shadow DOM (<code>host-ref="shadow"</code>):</strong>
  <include-content is-active host-ref="shadow">
    <template>
      <style>h4 { color: tomato; }</style>
      <h4>I'm tomato — outer styles can't touch me</h4>
      <p>Encapsulated inside an attached shadow root.</p>
    </template>
  </include-content>
</div>

"iframe" paints into a child <iframe data-render-host> you provide — sandbox styles / scripts / document context. Only nodes move: custom elements upgrade there only if the iframe document loads their definitions:

<include-content is-active host-ref="iframe">
  <template>
    <style>
      body { font-family: system-ui; padding: 12px; color: #333; }
      h4 { color: navy; margin: 0 0 4px; }
    </style>
    <h4>Hello from inside the iframe</h4>
    <p>Rendered into <code>iframe.contentDocument.body</code>.</p>
  </template>
  <iframe data-render-host
    srcdoc="<!doctype html><html><body></body></html>"></iframe>
  <style>
    #demo-renderable-element-host-iframe > :first-child {
      display: block;
      border: 1px dashed currentColor;
      padding: 8px;

      > iframe {
        width: 100%;
        height: 140px;
        border: none;
        background: white;
      }
    }
  </style>
</include-content>

Any other value is a CSS selector — the template lands in whatever element it resolves to:

<p>Any other <code>host-ref</code> value is treated as a CSS selector. The element renders its template into whatever the selector resolves to.</p>

<style>
  .selector-demo {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
  .selector-demo .mount-point {
    border: 1px dashed currentColor;
    padding: 8px;
    min-height: 80px;
  }
</style>

<div class="selector-demo">
  <div>
    <strong>Source element:</strong>
    <include-content is-active host-ref=".sidebar-mount">
      <template>
        <p>I render into <code>.sidebar-mount</code> →</p>
      </template>
    </include-content>
  </div>
  <div>
    <strong>Mount target:</strong>
    <div class="mount-point sidebar-mount"></div>
  </div>
</div>

On a prerendered page the light DOM and a selector host keep their prerendered content; shadow and iframe hosts are not prerendered and render in the browser.

Hooking render with view transitions

render and unrender are cancelable; event.detail is the update thunk. A parent can preventDefault() and run the mutation inside document.startViewTransition() — the same pattern <spa-manager> uses to batch sibling routes.

<p>Toggle the checkbox to activate the element. The cancelable
<code>include-content-render</code> event is intercepted, the actual
DOM mutation is wrapped in <code>document.startViewTransition()</code>,
and a parent listener can defer or skip the render entirely.</p>

<style>
  .render-event-demo {
    border: 1px solid var(--border, #444);
    padding: 12px;
    border-radius: 4px;
  }
  .render-event-demo include-content {
    display: block;
    margin-top: 8px;
    padding: 8px;
    border: 1px dashed currentColor;
  }
  .render-event-demo include-content > article {
    background: rgba(255, 200, 80, 0.15);
    padding: 12px;
    view-transition-name: render-demo-card;
  }
  ::view-transition-old(render-demo-card),
  ::view-transition-new(render-demo-card) {
    animation-duration: 350ms;
  }
</style>

<div class="render-event-demo">
  <label>
    <input type="checkbox" />
    Active
  </label>

  <include-content>
    <template>
      <article>
        <h4>Just rendered (with a view transition)</h4>
        <p>The host swallowed the default render and ran it through <code>startViewTransition</code>.</p>
      </article>
    </template>
  </include-content>

  <quark-sheet>
    @use "/demo-utils" as *;

    label {
      @on change { + include-content { is-active: event.target.checked; } }
    }
    include-content {
      @on include-content-render, include-content-unrender (handle: renderInTransition);
    }
  </quark-sheet>
</div>

The thunk's returned Promise resolves when the view is ready (immediately, or when ready-on fires). If is-active is unset while still loading / delaying-ready, teardown rejects that Promise and emits aborted instead of unrender.

Pair with ready-on so delaying-ready stays set until your transition has committed:

<media-gated ready-on="my-app-paint" media-query="(min-width: 900px)">
  <template>...</template>
</media-gated>
media-gated[delaying-ready] {
  display: none;
}

Persisting content across cycles

Once a template resolves, did-load stays set so consumers know later toggles are warm — URL template-refs reuse the shared fetch cache in kit-utils. Without persist-content (the default), each activation re-resolves and imports a fresh clone — subtree state is lost on unrender. With it, the same live nodes are held across toggles:

<p>Type into both inputs, then toggle them off and on. The right side
keeps its value because <code>persist-content</code> reuses the same
template node instead of cloning a fresh copy each render.</p>

<style>
  .persist-demo {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
  .persist-demo include-content {
    display: block;
    border: 1px dashed currentColor;
    padding: 8px;
    margin-top: 4px;
  }
</style>

<div class="persist-demo">
  <div>
    <strong>Default (cloned each render):</strong>
    <label>
      <input type="checkbox" checked />
      Active
    </label>
    <include-content is-active>
      <template>
        <input type="text" placeholder="Type, then toggle off + on" />
      </template>
    </include-content>
  </div>

  <div>
    <strong><code>persist-content</code>:</strong>
    <label>
      <input type="checkbox" checked />
      Active
    </label>
    <include-content is-active persist-content>
      <template>
        <input type="text" placeholder="Type, then toggle off + on" />
      </template>
    </include-content>
  </div>

  <quark-sheet>
    label {
      @on change { + include-content { is-active: event.target.checked; } }
    }
  </quark-sheet>
</div>

On a prerendered page the nodes it holds are the prerendered ones.

Loading strategies

pre-fetch controls when the template is fetched, separately from when it is rendered:

<!-- default: fetch on first activation -->
<my-el></my-el>

<!-- pre-warm immediately on attribute set -->
<my-el pre-fetch="eager"></my-el>

<!-- backfill on idle -->
<my-el pre-fetch="idle" template-ref="/fragments/hero.html"></my-el>

Pair with bypass-cache for revalidation when the element activates multiple times. Invoke the --reload command to force a refresh.

pre-fetch="idle" never runs in a prerender: the template is fetched in the browser.

Release notes

0.3.0 (2026-10-07)

  • Add hydration of prerendered content: an element whose host already holds the server's render of the same template does not render again, fires did-render once and is ready at once (never delaying-ready); persist-content keeps those nodes across toggles and pre-fetch="idle" does not run during a prerender
  • Fix an issue where a first mount into a host-ref selector cleared the host and fired did-unrender: what the host holds now stays until the first render replaces it

0.2.0 (2026-09-30)

  • Update a URL template-ref that answers with an error status to set is-error and fire the error event, paint nothing and log one error with the status as its cause; the error page is no longer rendered as content
  • Fix an issue where a failed template load held ready until render-timeout: the promise from render rejects at once and delaying-ready clears
  • Fix an issue where the promise from render never settled and delaying-ready stayed set when host-ref resolved to no host: the promise now rejects at once and delaying-ready clears, while an author iframe still loading keeps waiting

0.1.1 (2026-09-23)

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

Full documentation: https://nucleus.excom.dev/packages/renderable-element