← Home

A native component model

A component here is one self-contained chunk of HTML: a <style> with @scope, the markup, and a <script>. The page fetches it and mounts it with streamHTMLUnsafe({ runScripts: true }), so the component's script runs as it streams in and wires up its own behavior: no framework, no build step.

This is the imperative path. Declarative streaming (the other demos) would inject the markup and scoped styles but not run the script, which is exactly why runScripts: true exists.

Live demo

The component streams in automatically after a short delay; its scoped styles apply and its counter button works, proving the script ran.

Component
<style>
  @scope {
    .widget { padding: 1rem; background: #eef2ff; }
    button { color: #4338ca; }
  }
</style>

<div class="widget">
  <h3>Counter</h3>
  <button>Clicked <span class="count">0</span>×</button>
</div>

<script type="module">
  // module scope, runs with runScripts: true
  const w = document.querySelector(".widget");
  let n = 0;
  w.querySelector("button").onclick = () =>
    (w.querySelector(".count").textContent = ++n);
</script>
How it's mounted
const res = await fetch("./stream"); // the server streams the component

// runScripts: true lets the component's <script> run as it streams in
res.body
  .pipeThrough(new TextDecoderStream())
  .pipeTo(mount.streamHTMLUnsafe({ runScripts: true }));
Live result (streamed)

Registering service worker…

How this works

The frame loads app.html, which on load fetches the component from the service worker (./stream, streamed after a short delay) and pipes it into the mount with streamHTMLUnsafe({ runScripts: true }).

Because runScripts is on, the component's <script> runs as it arrives and wires up the button, and @scope keeps its styles local to the mount. The log line "✓ component <script> ran" is printed by that script.