CODE CRISPIES

Web Components in 2026 — Finally Worth Using

3 min read htmljavascriptarchitecture

Web Components have been "the future" for a decade. The reason adoption stayed niche: they were missing pieces. Server-rendering was hacky. Composition fought you. Forms didn't integrate. By 2026, every one of those got fixed. They're finally worth reaching for.

Declarative Shadow DOM (server-renderable)

The biggest blocker was: components only existed after JS ran. SSR was painful, hydration was custom. Declarative Shadow DOM solves it natively:

<my-card>
  <template shadowrootmode="open">
    <style>
      .card { padding: 1rem; border-radius: 8px; background: var(--bg, white); }
    </style>
    <div class="card">
      <h2><slot name="title"></slot></h2>
      <p><slot></slot></p>
    </div>
  </template>

  <span slot="title">Crispy Cereal</span>
  Stays crunchy in milk for 3 minutes.
</my-card>
html

The <template shadowrootmode="open"> is parsed by the browser into a real shadow root without JavaScript. Server-rendered, search-indexable, no flash-of-unstyled-content.

Scoped Custom Element Registries

Pre-2024 problem: only one global registry. customElements.define("my-card", ...) from library A and library B → conflict.

const registry = new CustomElementRegistry();
registry.define("my-card", MyCard);

document.querySelector("#scope1").attachShadow({
  mode: "open",
  customElements: registry
});
js

Each shadow root can have its own registry. Two libraries can ship a <my-card> and they don't collide.

Form-Associated Custom Elements

Custom elements can finally participate in <form> like a real <input>:

class MyToggle extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this.internals_ = this.attachInternals();
    this.attachShadow({ mode: "open" }).innerHTML = `
      <button type="button" part="toggle">
        <slot></slot>
      </button>
    `;
    this.shadowRoot.querySelector("button").addEventListener("click", () => {
      this._on = !this._on;
      this.internals_.setFormValue(this._on ? "on" : "");
    });
  }

  get value() { return this._on ? "on" : ""; }
  set value(v) { this._on = v === "on"; }
}
customElements.define("my-toggle", MyToggle);
js
<form>
  <my-toggle name="notifications">Notifications</my-toggle>
  <button>Submit</button>
</form>
html

The <my-toggle> submits with the form like a regular checkbox. Form validation, reset, autofill — all integrated. Before this, you needed a hidden <input> shim.

CSS ::part() for safe styling

Shadow DOM was infamous for "I can't style the inside of someone else's component." ::part() is the bridge:

<!-- consumer side -->
<my-toggle name="notifications">Notifications</my-toggle>

<style>
  my-toggle::part(toggle) {
    background: var(--brand);
    border-radius: 999px;
  }
</style>
html

The component author exposes parts (part="toggle" in the shadow). The consumer styles those parts only. No "we override your private internals and break on next release" coupling.

CSS custom properties penetrate Shadow DOM

my-card {
  --bg: #fef3c7;
}
css

Custom properties cross shadow boundaries by default — the perfect themeing escape hatch. Combined with ::part(), you can theme an external library without touching its internals.

When to reach for them

When NOT to

The 2026 stack

For new component libraries, the modern minimal stack is:

That's a compete UI library in 5 KB. No virtual DOM, no reconciler, no compile step beyond optional TS.


Practice modern HTML in the html-elements module on Code Crispies — covers semantic elements + dialog + popover.