In 2004, the W3C was betting HTML's future on XHTML 2: a stricter, XML-based language that wasn't backward compatible with the web people had actually built. Apple, Mozilla, and Opera disagreed, and formed the WHATWG to keep evolving HTML instead. That work became HTML5.
And for a while, it was thrilling. HTML5 wasn't just a spec; it was a movement. We got <video> and <audio> (goodbye, Flash), <canvas>, new form input types, semantic elements like <article> and <nav>, and a parsing algorithm that finally made every browser read broken markup the same way. There was a logo. HTML had a logo. There were whole conferences about it. The message was clear: HTML was alive again, and it was going to grow up into an application platform.
HTML5 became a W3C Recommendation in 2014. And then the energy mostly went somewhere else.
Look at what happened to the other two languages over the next decade:
JavaScript went from a language people apologized for to one that ships a new edition every year. ES2015 alone gave us classes, modules, promises, arrow functions, and destructuring. TC39 built a stage process so public that you can watch a feature grow from a napkin sketch into a shipped standard.
CSS may have had the best decade any language has ever had. Grid. Custom properties. Container queries. :has(), the parent selector we were told for years was impossible. Native nesting. Cascade layers. @scope. Describe 2024's CSS to a developer in 2014, and they'd assume you were describing a preprocessor. (I should know. I've been maintaining Less.js for over 10 years.)
And HTML? HTML got crumbs.
A <dialog> element that took until 2022 to work in every major browser. A popover attribute. A <search> element for marking up search forms. Invoker commands, so a button can open a dialog without a line of JavaScript. Every one of these is welcome. But every one of them is a widget: a single, pre-built behavior handed down by the spec. None of them is a primitive: something you can build your own things out of.
HTML is now a "Living Standard," which is a lovely name. But living isn't the same as growing. JavaScript and CSS got new capabilities; HTML got new nouns. A decade on, markup still has no way to say "repeat this," "show this only if," "this value comes from over there," or "this is a component, and here's its contract." For any of that, you leave HTML and go write JavaScript.
Web Components were supposed to fill that gap. And to be fair, they shipped. They work. GitHub, YouTube, and Salesforce all use custom elements in production, and some of them use them well.
But they were meant to be more than that. They were meant to be the web's component model: write a component once, use it anywhere, no framework required. More than a decade later, ask HTML for a reusable, typed component with conditionals, loops, and reactive state, and the answer is the same as it was in 2013: "install a framework." That's the sense in which they failed.
Some context: I've been a professional web developer for over 20 years. I've written plenty of custom elements. I've written a lot more React, Vue, and Svelte components. And I've watched a lot of teams (including mine) try to build a "framework-agnostic" component library on Web Components and quietly give up.
Here's how that played out.
The promise, and the retreat
Web Components were first pitched in 2011, and Google's Polymer library became their flagship: the platform as the framework. Polymer eventually went into maintenance mode in 2018 and was superseded by Lit.
Lit is genuinely good. But the transition tells you something. The people most invested in Web Components concluded that the raw custom-element API needed a rendering library on top of it. That was a reasonable call; it was also an admission that the platform alone wasn't enough.
Framework authors looked elsewhere
Meanwhile, the people building component models for a living were reaching their own conclusions. In 2019, Rich Harris wrote "Why I don't use web components". If you haven't read it, the short version:
- Shadow DOM pushes your CSS into JavaScript.
- The property/attribute split forces boilerplate.
- Slotted content renders eagerly, whether you want it or not.
- There's one global registry, so naming collisions are your problem.
- The DOM is, in his words, "an awkward interface for building interactive applications."
You can debate any one of those points. The outcome is harder to debate: React, Vue, Svelte, and Solid each built their own component model rather than building on this one. The people who design component models for a living chose not to use the platform's.
A button you can't extend
Even if you skip the frameworks and go all in on the platform, you hit walls quickly. The one that surprises people most: Say you want a button with some extra behavior. The spec has an answer for this! It's called a customized built-in:
<button is="fancy-button">Save</button>
class FancyButton extends HTMLButtonElement {
// extra behavior here
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
Great! It's still a real <button>. It submits forms, it's focusable, it has the right role, Enter and Space work. Ship it!
Welp, not so fast. That works in Chromium and Firefox. WebKit's formal standards position is to oppose customized built-ins and not implement them. So in Safari, you get a plain button that ignores your class entirely.
"Fine," you say, "I'll use an autonomous custom element."
<fancy-button>Save</fancy-button>
That works. But now you have an element that doesn't submit forms, isn't focusable, has no button role, and doesn't respond to the keyboard. So you add tabindex, role="button", a keydown handler for Enter and Space, ElementInternals for form association, disabled-state handling...
At this point you've reimplemented <button>, and you'll be maintaining that reimplementation for as long as the component lives. The browser already had a perfectly good one; the platform just didn't give you a way to use it.
The only spec-sanctioned way to reuse native semantics has no cross-browser path. That's not an edge case; that's the core use case.
Ten years to catch up
Interop was the other half of the promise, and for about a decade, the relationship status between React and custom elements was "it's complicated." React couldn't pass an object or array to one as a property, and couldn't listen to its custom events without manual ref wiring. The gap was tracked publicly on Custom Elements Everywhere for years.
Full support landed in React 19, in December 2024. That's more than ten years after custom elements were proposed. "Use it anywhere" didn't include the most popular UI library for most of that time.
The platform's own basics took nearly as long:
- Form participation via
ElementInternalsonly became stable cross-browser with Safari 16.4, in March 2023. - Declarative Shadow DOM (i.e. server rendering a shadow root without JavaScript) reached Safari in 16.4 and Firefox in 123, in 2024.
Forms and SSR. The two things every real app needs. Roughly a decade to get there.
So what actually went wrong?
Honestly, I don't think it matters whether Web Components failed by design or just because they were never fully leveraged. If you're picking a component model today, the outcome is the same: in their shipped form, they didn't become the web's component layer.
But I do think there's a single root cause, and it's this:
Web Components solved packaging. They never solved authoring.
They gave us a place to put a component (a tag name, a lifecycle, a shadow root). They never gave us a way to write one in markup. Look at what an app author reaches for every day, and what HTML gives you natively:
| What you need | What HTML gives you | What everyone does instead |
|---|---|---|
| A reusable component with a typed API | Custom Elements (untyped, imperative, string-only attributes) | React / Vue / Svelte components |
| Conditional and repeated markup | Nothing |
v-if, {#each}, .map()
|
| Reactive state | Nothing | hooks, refs, runes, signals |
| Bind an input to state | Nothing | controlled inputs, v-model, bind:value
|
| Declare a data dependency | Nothing | fetch-in-effect, loaders, resources |
Every cell in that last column solves the same problem with different syntax: a loop is v-for in Vue, {#each} in Svelte, and .map() in React. Same idea, three syntaxes, and none of them carry over. That's why your component library gets rewritten every time your company changes frameworks. Web Components didn't fix that, because they never tried to.
This is the HTML5 story again. JavaScript and CSS each got a sustained, decade-long investment in the language itself. HTML's component model, by contrast, was built from the JavaScript side: a class you register, with HTML as the place it gets mounted. The markup never learned anything new.
Shadow DOM added a subtler problem. Most of us wanted scoping: my component's styles shouldn't leak out. What we got was isolation: nothing gets in either, so theming, shared design tokens, forms, and focus all have to negotiate with the boundary. We asked for scoping and got isolation along with it.
We can fix them
Here's the part where I stop complaining. If you take the record above seriously, the fix has a pretty clear shape:
- Write components as markup, not as imperative JavaScript classes. Templates, conditionals, loops, bindings: in HTML.
-
A component should lower to a real native element. If it's a button, the browser should see a
<button>. No wrapper, no rebuilt semantics. -
Scope styles without a shadow boundary. CSS
@scopeexists now. Encapsulation without isolation; real isolation becomes opt-in, not a tax. -
Reactivity should be a declarative dependency graph that tools can read, type, and analyze. No
eval, nonew Function, CSP-clean. - It should be portable. Compile the same source to React, Vue, or Svelte, so it isn't Yet Another Walled Garden.
- Graduate into the platform one piece at a time. That's how Declarative Shadow DOM and invoker commands went from userland shims to shipped syntax.
None of these ideas are new on their own. Vue and Svelte proved markup-first components and scoped styles. Mitosis proved one source, many targets. The missing piece is putting them together as HTML, with the explicit goal of getting them into the platform.
Enter HTML Next
HTML Next is a component format, plus open source (MIT) tooling, built on a public Declarative HTML Components proposal. Here's a complete component:
<template component="x-counter">
<defs>
<state name="count" type="number" value="0"></state>
<handler name="increment">
<set name="count" expr:value="count + 1"></set>
</handler>
<handler name="reset">
<set name="count" expr:value="0"></set>
</handler>
</defs>
<div>
<button type="button" on:click="increment">
Count: <span $value="count"></span>
</button>
<button type="button" on:click="reset">Reset</button>
</div>
</template>
That's it. Markup, typed state, and event handlers in one HTML file. No class, no connectedCallback, no attributeChangedCallback, no shadow root.
And remember the button problem? Here's what happens to a component invocation:
<!-- You write -->
<x-button variant="solid">Save</x-button>
<!-- Replaced with a real button at runtime -->
<button data-component="x-button">Save</button>
The component doesn't pretend to be a button and hope every browser plays along. It gets replaced with an actual <button>. No customized built-ins, so no waiting on Safari, from the people who make iPhones (who are famously against buttons). Form submission, focus, role, and keyboard behavior stay the browser's job, where they belong.
You can use that same source file:
- natively, with a small browser runtime (or with Vite), or
- in Vue, React, or Svelte, imported alongside your existing components through Vite adapters, or
- as generated framework source, via the converter, if you'd rather ship plain React/Vue/Svelte code.
That last part is the bit I care most about. A component library shouldn't need a rewrite every time an app changes frameworks. Write it once in HTML, and let the consuming project decide.
This is a dev.to article, so it's probably not going to surprise you that I'm the editor of this proposal, and you should weigh my enthusiasm accordingly.
The proposal is at Stage 0. The tooling is early, and syntax and output may change. It is not a W3C or WHATWG deliverable, and browsers don't understand this syntax on their own; the tools run or compile it for you. The goal is to prove features in userland and then upstream the ones that earn it, feature by feature. Some things a compiler simply can't fix (like native element extension across browsers), and those belong in later proposals to the actual standards bodies.
The proposal behind it
The tooling is the part you can use today. The part I'd most like your eyes on is the spec it implements: Declarative HTML Components Level 1.
It's written the way platform specs are written, because the end goal is to become one. It covers templating ($each, $if, $match), a small type system, a pure expression language with Intl formatting, bindings and events, slots and composition, server-rendered markup that hydrates into the same instance, reactivity and data, validation, style scoping, and a security chapter (no eval, static resolution).
A few design decisions I think are worth calling out:
- Conformance is observable equivalence. A compiler and a browser polyfill are both valid implementations, and they must produce the same native DOM, state, events, accessibility, and errors for the same source.
- It versions like the platform does. Levels like CSS (this is Level 1), stages like TC39 (this is Stage 0), and dated snapshots in between. No big-bang "1.0."
- It's purely additive. A plain HTML file is already valid. You adopt one feature, one tag at a time.
- It names what it can't fix. Native element extension and a better Declarative Shadow DOM are explicitly marked as upstream work for the real standards bodies.
There's also an honest list of open questions: data fetching and caching semantics, SSR serialization, whether expressions should use and/or or &&/||, scheduling, and parser edge cases like tables and SVG. If you have opinions on any of those (and if you've read this far, you probably do), open an issue on the specs repo. That feedback shapes what Level 1 becomes.
Moving forward
I don't want to bury Web Components. A lot of smart people put a decade into them, and pieces of that work (custom element registration, ElementInternals, Declarative Shadow DOM) are genuinely useful. I want to finish them: give the platform the authoring layer it skipped.
If that sounds interesting, read the proposal, build your first component, poke at the source, and file issues when something breaks. Something will. That's what Stage 0 is for.
In 2004, a few people at three browser companies decided HTML deserved a better future than the one it was being handed, and they went and built it. The whole web got better for it, quickly. JavaScript and CSS have had that kind of energy for ten years straight. I think it's HTML's turn again, and this time, anyone can help write it.
Top comments (1)
A while back I put together a small language project. My needs were simple: I need to show [data], in a lot of different tables, in a lot of different ways.
I was unfamiliar with Web Components, and so I built the tool with Lit. I'm not mad with Lit, necessarily. It was a good framework. But I was annoyed with web components.
I was wanting a templating solution; something where I could write the markup once because the structure would be used a lot. What I got was styles written in JavaScript. I didn't want that at all! Now I'll admit that the mistake was mine; I chose Lit when Web Components just weren't what I needed. Lit was the wrong solution.
But then some time later I wanted to work on a small project. This time, no Lit. But I was going to try web components again. That's when I realized that I couldn't extend Tables! If there's something that you're going to need templating solutions for in HTML, it's gonna be tables (that's probably why there's a Table API). I can't tell you how annoying it is that ... we can't do that. If you want a web component for a table, what you're doing is creating a wrapper
divfor a table and then querying down to that table so that you can use the table API. So... that's not helpful.and pushing web components to the side for just a moment…
Chances are, if you've got a valid reason to be using React, Vue, Svelte, or whatever ... that reason is, "there's state." We're not talking about all the goofballs using React to generate static sites here. We're talking about the legitimate instances where you've got an app and things in that app need to change based on user behaviors.
So you need state. I think most of us would be happy to solve most basic interactive problems with a dead-simple State API that could live on top of any HTML elements.
And yes ...after state, we really want basic templating features that can help us write DRY HTML. It's an uncomfortably tedious process to coerce web components into being the kind of template that comes so easily from Vue or Angular. And when you do, yep, there's isolated styles there to be a problem!
TL;DR I appreciate you sharing your sentiment and I am excited that there are folks trying to still push HTML forward.