I'm building a small status indicator to reuse across my projects. A plain page, a framework application, whatever I build next. It needs a label, a state and some styles, and requiring the surrounding application to adopt its framework feels like a lot to ask of a coloured pill.

That is the small end of the bet. The bigger one is a framework assembled from useful components, with the page loading each one's JavaScript when it needs it. I'd like the browser to be the place they meet.

With a custom element I can name a tag, put a styling boundary around its markup using Shadow DOM, and leave a slot for the caller's label.

That's enough for this pill.

Colouring it green can leave the application's data fetching alone. Here's a complete illustrative element:

<status-pill state="ok">Build worker</status-pill>

<script type="module">
class StatusPill extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" }).innerHTML = `
      <style>
        :host {
          display: inline-block;
          padding: .2em .7em;
          border: 1px solid currentColor;
          border-radius: 1em;
        }
        :host([state="ok"]) { color: #176b38; }
        :host([state="waiting"]) { color: #795300; }
      </style>
      <slot></slot>
    `;
  }
}
customElements.define("status-pill", StatusPill);
</script>

The whole thing on one screen

Your label stays in the page's ordinary DOM. The slot is where that content appears inside the component. The styles respond to the state attribute, so this example doesn't need a JavaScript rendering function each time the state changes.

Change state to waiting. The colour changes and the label still says Build worker.

There is no bundler in that example. The browser loads the module and registers the tag.

I can see the whole thing on one screen, which is about as much setup as I want for a coloured pill.

Where the component meets the page

Compare the jobs involved in that pill with the jobs involved in a whole application:

DecisionA useful place to own it
Indicator markup and internal stylesThe element
Which state to showThe surrounding application
Loading data and handling navigationThe application architecture
Meaning conveyed by the indicatorThe component's public interface and its caller

On a build page, the application checks whether the build succeeded. The pill gets state="ok". It goes green. That's its whole contribution to the build.

A plain page and a framework application both have attributes, properties and DOM events, so values can come from the page already there while I decide when and how they get passed in. I don't want the pill choosing the rest of their application. Strings fit naturally in attributes and larger objects belong in properties. A property can take an object. No conversion into text. An event needs a name and a payload callers can understand, I can see myself spending longer on that name than on the coloured pill, renaming it, trying it out loud, deciding none of them sound right.

I attach the shadow root once, in the constructor. An element can be disconnected and connected again. Attaching a second root on each connection would be an unfortunate way to discover its lifecycle. The custom-element specification defines the callbacks and constructor requirements.

Shadow DOM makes accidental styling less likely. A page rule for span won't casually restyle every internal span in the component, while inherited properties and CSS custom properties can still cross the boundary to help it follow a theme. Keeping every page's CSS out would be less useful if I couldn't make the pill match that page, so custom properties could expose its colours for the caller to choose. A larger component might expose particular internal parts for styling. Hiding the CSS still leaves me an interface to design.

The tag can be in the document before its JavaScript arrives. That changes how I picture loading the application. The page can already show useful content, then gain a component's behaviour when there's a reason to load it.

Move the class and its registration from the example above into status-pill.js. This page can activate the pill on a click:

<status-pill state="ok">Build worker</status-pill>
<button id="load-status" type="button">Load status component</button>

<script type="module">
document.querySelector("#load-status").addEventListener("click", async () => {
  await import("./status-pill.js");
});
</script>

The dynamic import fetches and runs the module. Registering the tag upgrades the existing pill in the document. Its label was there all along. Now it gains the component's styling. I haven't had to start the page again.

In my head, a larger application could work like this too. Open the chart and load the chart component. Open an inspector and load that. If I don't use either, I don't download either one's JavaScript. I want to try this with something bigger than a status indicator.

It also gives me somewhere to start with hot reloading. A development server could send the changed component's code while the rest of the page stays open, and I'd rather keep what I'm typing than reload the application to admire a new border radius.

Hot reloading needs an update mechanism of its own. The custom-element registry allows a tag to be defined once, and importing the same module URL again uses the cached module. Replacing a registered component means arranging how its existing instances update and what state they keep. Late activation gives me a useful boundary for designing that replacement.

Webpack already supports code splitting and hot module replacement. My bet is on the shared browser interface, so the component can travel between applications without bringing a particular framework's runtime with it.

The network is moving too

One reason to combine scripts is to avoid paying for lots of requests. A single bundle is a convenient delivery unit. It can also make me download an inspector before I've decided to inspect anything.

HTTP/2 already lets requests share a connection, with their data travelling in separate streams. That's multiplexing.

Underneath, TCP still delivers one ordered sequence of bytes, so several unrelated requests can be held up by one missing packet while that gap is repaired.

HTTP/3 uses QUIC, a transport built over UDP, to give streams independent delivery so data lost from one stream needn't stop delivery on the others. The current HTTP/3 draft describes that difference. The streams still share the available bandwidth. Sharing a connection without that same wait behind a missing packet appeals to me.

This is already being deployed. Cloudflare announced HTTP/3 support in September 2019, and Chrome is rolling it out. HTTP/3 is still a draft, with implementations I can already try.

This makes me less keen on the enormous bundle you download before doing anything. If the browser can fetch a chart without a lost packet holding up the inspector too, loading those components separately becomes more appealing. That's what I'm betting on here.

Grouping code that's usually needed together can still save work. Separate files have request overhead, and a chain of imports can leave the browser waiting to discover the next one. QUIC changes how the files travel. Those dependencies and the cost of running JavaScript stay exactly where they were.

What needs to arrive together?

The framework I picture is a set of optional JavaScript packages. A page chooses the components it uses and loads them as needed. The packages could come from the same server. Their independence is about how they fit into the page, without requiring separate hosts or a new network connection for each one.

That's the larger bet for my next application, a framework that helps me build these components and loads them as needed. I haven't chosen the application yet. I'm already thinking about which parts could be shared with the one after it, and the one after that.... Some people plan holidays.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.