Short answer: yes, but not everywhere. As of July 2026, native custom elements, Shadow DOM, and declarative shadow DOM let you build components that hold up in real products, especially if you're maintaining a design system shared across multiple frameworks. But for app-level state management and deep two-way data binding, they still trail React and Vue.
This piece asks "can I actually ship a real product with Web Components?" without pretending the answer is a framework-agnostic silver bullet. We walk through the fundamentals, how much the SSR gap has actually closed, where the friction lives when you mix Web Components with React and Vue, and where they genuinely win as a design-system layer.
The fundamentals: custom elements, Shadow DOM, and slots
A Web Component is built from three pieces. Custom elements register a new HTML tag with the browser via a call like customElements.define('product-card', ProductCard), after which <product-card> works on any page. Shadow DOM isolates the element's internal markup and styles from the rest of the page, so a .title class inside the card never collides with a same-named class elsewhere. Slots let you inject content from the outside: write <product-card><span slot="price">$29</span></product-card> and the <slot name="price"> in the component's template renders that content in place.
Together, these three produce a genuinely encapsulated component that no framework owns. MDN's Web Components documentation still covers the full API surface and remains the most reliable reference.
class ProductCard extends HTMLElement {
static formAssociated = false;
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; }
</style>
<div class="card">
<slot name="title"></slot>
<slot name="price"></slot>
</div>
`;
}
}
customElements.define('product-card', ProductCard);Has the SSR gap actually closed? Declarative Shadow DOM
Web Components' biggest historical weakness was server-side rendering: Shadow DOM used to be attachable only in the browser after JavaScript ran, so the initial HTML arrived as an empty shell. Declarative shadow DOM fixes this with a <template shadowrootmode="open"> tag: you embed the shadow root directly in the HTML, and the browser attaches the shadow tree while parsing the page, with no JavaScript wait required.
<product-card>
<template shadowrootmode="open">
<style>.card { border: 1px solid #ddd; padding: 1rem; }</style>
<div class="card"><slot name="price"></slot></div>
</template>
<span slot="price">$29</span>
</product-card>The feature now ships natively in Chrome, Firefox, Safari, and Edge, and web.dev's declarative shadow DOM article frames it as a real win for Core Web Vitals: content becomes visible at first paint without waiting on client-side JavaScript. That's a genuine gap closed, not just marketing, but it isn't a complete fix on its own. It plays naturally with static or islands-style tooling like Astro or Eleventy; wiring it into a fully React-tree-owned SSR pipeline like Next.js still takes extra bridging code.
Living with React and Vue: where the friction remains
Dropping Web Components into a React or Vue project is not as frictionless as the pitch suggests. Three points genuinely rub.
Complex props and objects
HTML attributes are strings. Pass an array or object to a custom element and React's <product-card data={obj}> syntax doesn't do what you expect, it gets serialized as a string attribute rather than assigned as a DOM property. The fix is assigning the DOM property directly (ref.current.data = obj) or leaning on a reactive property system like Lit's. This friction with React's declarative JSX model usually means writing a thin wrapper component.
Custom events versus the synthetic event system
Custom elements communicate state via CustomEvent, while React runs its own synthetic event system and auto-wires props like onChange only to known DOM events. Before React 19, you needed manual addEventListener calls; React 19's native custom-element support smoothed this bridge somewhat, but Vue and Svelte each need their own small adaptation. Event-model differences are a real factor in framework choice, a topic we cover more broadly in HTMX vs React: When to Use Each.
Form participation: ElementInternals
A native <input> is part of a form by default; a custom element is not. The ElementInternals interface and attachInternals() API solve this: set static formAssociated = true to make an element form-associated, then call internals.setFormValue(value) to report a value to the form. WebKit's ElementInternals blog post shows the API also covers validation and accessibility states. Still, it's an extra layer you build from scratch, and wiring it into libraries like React Hook Form remains a manual job.
Reuse as a design system: build once, consume everywhere
This is where Web Components clearly win. Write a button or card as a native custom element, and React, Vue, Svelte, and plain HTML pages can all consume the exact same component, because the target is the browser itself, not a framework API. If five teams inside one company use five different frameworks, shipping the design-system library as Web Components removes the "update the React version, don't forget Vue" synchronization tax. For teams that keep state management outside the component, behind a thin attribute-plus-event contract, this is a real win. The same logic pairs well with other native, framework-independent browser capabilities, like the transitions covered in our View Transitions API how-to.
Aspect | Web Components | React components |
|---|---|---|
SSR support | Native via declarative shadow DOM | Framework-dependent (Next.js, RSC, etc.) |
Bundle size | Small, no framework runtime | Requires React + ReactDOM runtime |
Framework interop | Works everywhere (React/Vue/Svelte/plain HTML) | Native only inside the React tree |
State management | Local and limited, needs an add-on library for global state | Mature ecosystem: Context, Redux, Zustand |
Form participation | Possible via ElementInternals, wired by hand | Native, works smoothly with libraries |
Best for | Shared design systems, embeddable widgets | App-level complex logic |
Where they win, and where they still hurt
Web Components are a clear choice when you're maintaining a design system shared across multiple frameworks or teams, building embeddable third-party widgets (a live-chat box, a payment form, a banner), or shipping UI that needs to outlive framework churn. If you need a component to still work in five years, writing it against a native browser API is a safer bet than writing it against a framework that might hit a breaking major version in three.
On the other hand, complex app-level state management, deep two-way data binding, and teams already fully committed to one framework's ecosystem all make Web Components extra overhead rather than a shortcut. Rebuilding, from scratch with custom elements, the developer experience that mature solutions like Context, Redux, or Zustand already provide, as covered in our React state management comparison, is often wasted effort. On the reactive-state side, signal-based approaches build an interesting bridge here, a topic we explored separately in Frontend Signals Explained.
Here's the honest take: Web Components win as an implementation detail inside a design system, not as a paradigm for building an app on their own.
For more frontend guides, browse the Web Development category.
Frequently Asked Questions
Can Web Components replace React in 2026?
No, and they were never meant to. Web Components give you a standard for component encapsulation; React gives you an ecosystem for app-level state management, routing, and render optimization. They aren't competitors, they operate at different layers, and many teams run both side by side.
Does declarative shadow DOM work in every browser?
As of July 2026, yes, it's natively supported in current versions of Chrome, Edge, Firefox, and Safari. If you need to support older browsers, you may still need a polyfill layer, but projects targeting evergreen browsers don't need an extra library.
How do you pass complex data to a custom element?
Since attributes only carry strings, passing an object or array requires assigning the DOM property directly (element.data = {...}) rather than writing it as an attribute. In React, this is usually done through a ref; libraries like Lit automate it with reactive property declarations.
Can I use a custom element inside a form without ElementInternals?
You can, but the form won't automatically collect its value, so you'll need to sync it with a hidden <input>. With attachInternals() and formAssociated = true, you hand that synchronization to the browser and get validation and accessibility states managed natively as well.



