javascript-web-component-hydration · npm web-component-hydration 1.0.5

Placeholder in. Live element out.

Two ES modules. CustomHTMLElement is an HTMLElement subclass that clones a template into an open shadow root; hydrate() is the function that swaps server-rendered placeholders for instances of it.

Quoted from slogsdon/javascript-web-component-hydration at commit f98d643 (2019-08-06, tag v1.0.5). Nothing on this page was run in a browser while building the site.

the surface

CustomHTMLElement
Extends HTMLElement. Subclasses declare a tag name with a static is getter and are defined once per page with the static register(), which wraps customElements.whenDefined() and rejects when no name is set or window.customElements is missing.
hydrate(name, slotElementType?)
Finds every [data-template="name"] in the document and replaces it with a freshly created <name> element, carrying over the initial data. The optional second argument is the tag used for slot content created from that data — span by default.
onReady(callback)
A wrapper around DOMContentLoaded that calls back immediately if the document has already finished loading.

The constructor does three things in order: it looks up document.getElementById('template-' + is), calls this.attachShadow({ mode: 'open' }), and initialises a list of child element names it has already hydrated. On connectedCallback it appends a clone of that template's content to the shadow root, then hydrates any [data-template] descendants — in the light DOM and in the shadow root both — so nested components resolve too.

the markup contract

The library does no rendering on the server itself; it is written to pair with a service that does. It needs two things in the page: a client template whose id is template- plus the element name, and a server-rendered placeholder whose data-template attribute is that same name. The README's example template and its rendered form, quoted:

<template id="template-x-list-item">
  <li>
    <slot></slot>
  </li>
</template>
<div data-template="x-list-item" data-initial-data="content">
  <li>
    <slot>content</slot>
  </li>
</div>

The data (content) becomes the data-initial-data value and is inserted into the unnamed slot as a child. Named slots require the value to be URL-encoded JSON keyed by slot name; the README's named-slot variant:

<div data-template="x-list-item" data-initial-data="%7B%22label%22%3A%22content%22%2C%22foo%22%3A%22bar%22%7D">
  <li>
    <slot name="label">content</slot>
    <slot name="foo">bar</slot>
  </li>
</div>

How the data is decoded. hydrate() runs decodeURI() and then the substitutions PHP's urlencode() needs — %3D to =, + to space, %2F to /, %3A to :, %2C to , — and tries JSON.parse(). On success each key becomes an element with slot = key and the value as innerHTML. On failure the whole decoded string is treated as one unnamed slot's HTML. Note that this writes slot content with innerHTML: the initial data is treated as markup, not as inert text.

install

The package is web-component-hydration on npm; 1.0.5 is the latest version and was published on 2019-08-06. It has no dependencies, module points at index.js, and the source is three ES modules — custom-html-element.js, helpers.js, and an index.js that re-exports both.

The repository's README documents usage, not installation, so no install command is printed here. Its own examples import by relative path from ./node_modules/web-component-hydration/…, which is what the sibling PHP demo does: its examples/simple/js/package.json depends on web-component-hydration@^1.0.0 and its README runs cd examples/simple && yarn install. The lockfile there resolves the dependency to exactly 1.0.5.

one example, quoted

The whole client side of a component, from the README. First the element:

// x-list-item.js
import { CustomHTMLElement } from './node_modules/web-component-hydration/custom-html-element.js';

export class XListItem extends CustomHTMLElement {
  // define `XListItem`'s tag name
  static get is() { return 'x-list-item'; }
}

Then the entry point — register the element, then hydrate the placeholders that carry its name:

// main.js
import { hydrate } from './node_modules/web-component-hydration/helpers.js';
import { XListItem } from './x-list-item.js';

// register `XListItem` as a custom element
await XListItem.register();
// hydrate any server-side rendered `XListIem`s
hydrate(XListItem.is);

Order matters: hydrate() returns early if the element is not defined yet or if template-<name> is absent, so a hydrate call that runs before register() resolves does nothing at all. The PHP demo wraps the whole sequence in onReady() and awaits each register() before calling hydrate() for that component.

Both snippets are quoted as written, including the README's XListIem typo in the trailing comment. The snippets were not run on their own, but the PHP demo they come from was. It was served with php -S on PHP 8.5.11 from php-web-component-ssr at 00be84d, with version 1.0.5 of the package from npm (its checksum matches the demo's lockfile), and opened in Chromium, Firefox and WebKit. In all three, no data-template placeholder was left. Each element had a shadow root holding its template, and the page logged no errors.

One thing the README doesn't mention is that initial data is added even when the template has no slot for it. The demo's foo: bar became <span slot="foo">bar</span>. It is in the element's markup but never shown.

status and limits

Version 1.0.5, MIT licensed. The last commit that changes behaviour is 2019-08-06 (the v1.0.5 tag); the repository's newest commit, 2026-05-08, only adds a homepage link. It is published and unmaintained, not deprecated.

Two consequences follow from how it works, not from a defect.

Hydration needs JavaScript. The browser receives a div with copied children; the shadow root and the real custom element only exist after the module runs. That is the same trade the PHP page describes, seen from the other side.

It is not declarative shadow DOM. Neither this repository's code nor its README mentions <template shadowrootmode>; the shadow root here is created imperatively with attachShadow() in the constructor, in the browser.

where to go