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>htmlThe <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
});jsEach 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>htmlThe <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>htmlThe 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;
}cssCustom 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
- Cross-framework widgets — same component in React, Vue, Angular, plain HTML pages.
- CMS embeds — shipped as a single
<script>+ the consumer drops<your-widget>into their page. - Design systems at scale — the framework changes every 3 years, the components don't.
- 3rd-party SDK widgets — Stripe Elements, Hubspot forms, etc. — already do this for isolation.
When NOT to
- Inside a single React/Vue/Solid app — the framework's component model is faster to develop and integrates with the ecosystem.
- When SSR is critical and you can't ship declarative Shadow DOM yet — Cloudflare Workers, Vercel etc. all support it now, but some older platforms don't.
The 2026 stack
For new component libraries, the modern minimal stack is:
- Lit (~5 KB) — for templating + reactive attributes
- Declarative Shadow DOM for SSR
::part()+ CSS custom properties for themingform-associated+ ElementInternals for forms
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.
