Web Components have been “almost ready” for a decade. In 2026, they are genuinely production-ready. Every major browser supports Custom Elements, Shadow DOM, HTML Templates, and the Declarative Shadow DOM. Frameworks like Lit make authoring them painless. Companies including GitHub, Adobe, Salesforce, ING Bank, and SpaceX ship Web Components in production. The value proposition is simple: write a component once, use it in React, Vue, Angular, Svelte, plain HTML, or any future framework that hasn’t been invented yet.
Why Web Components Now
The web platform itself provides the component model. Custom Elements define new HTML tags. Shadow DOM provides style and DOM encapsulation. HTML Templates define inert markup for stamping. These are browser-native APIs, not library abstractions. A Web Component rendered in a React app behaves identically to one rendered in a static HTML page. No build step required. No runtime dependency. No version conflicts between framework X and framework Y. For design systems that need to serve multiple teams on different stacks, this is the only architecture that scales without coupling.
Your First Custom Element
class DwAlert extends HTMLElement {
static observedAttributes = ['type', 'dismissible'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() { this.render(); }
attributeChangedCallback() { this.render(); }
get type() { return this.getAttribute('type') ?? 'info'; }
get dismissible() { return this.hasAttribute('dismissible'); }
render() {
const colors = {
info: { bg: '#eff6ff', border: '#3b82f6', text: '#1e40af' },
success: { bg: '#f0fdf4', border: '#22c55e', text: '#166534' },
warning: { bg: '#fffbeb', border: '#f59e0b', text: '#92400e' },
error: { bg: '#fef2f2', border: '#ef4444', text: '#991b1b' },
};
const c = colors[this.type] ?? colors.info;
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
.alert {
padding: 12px 16px;
border-left: 4px solid ${c.border};
background: ${c.bg};
color: ${c.text};
border-radius: 4px;
font-family: system-ui, sans-serif;
display: flex; align-items: center; gap: 8px;
}
.content { flex: 1; }
button { background: none; border: none; cursor: pointer; color: ${c.text}; font-size: 18px; }
</style>
<div class="alert" role="alert">
<div class="content"><slot></slot></div>
${this.dismissible ? '<button aria-label="Dismiss">x</button>' : ''}
</div>`;
this.shadowRoot.querySelector('button')?.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('dismiss'));
this.remove();
});
}
}
customElements.define('dw-alert', DwAlert);
Use it in any HTML page or framework template:
<dw-alert type="success" dismissible>Your changes have been saved.</dw-alert>
<dw-alert type="error">Connection failed. Check your network.</dw-alert>
Shadow DOM: Real Encapsulation
Shadow DOM creates a scoped DOM subtree. Styles defined inside the shadow root do not leak out, and external styles do not leak in. This solves the global CSS problem that has plagued web development for decades. Your component’s .alert class will never collide with another .alert class anywhere on the page. No BEM, no CSS Modules, no scoped attribute hacks. The browser handles it natively.
You can still allow controlled external styling through CSS custom properties and the ::part() pseudo-element:
// Inside component: expose a CSS custom property
:host { --alert-radius: 4px; }
.alert { border-radius: var(--alert-radius); }
// Consumer overrides it:
dw-alert { --alert-radius: 12px; }
Scaling with Lit
Vanilla Custom Elements work, but get verbose for complex components. Lit (by Google) adds reactive properties, declarative templates, and efficient re-rendering in a 5KB library:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('dw-counter')
export class DwCounter extends LitElement {
static styles = css`
:host { display: inline-flex; align-items: center; gap: 12px; }
button { padding: 4px 12px; cursor: pointer; border-radius: 4px; border: 1px solid #ccc; }
span { font-variant-numeric: tabular-nums; min-width: 3ch; text-align: center; }
`;
@property({ type: Number }) count = 0;
@property({ type: Number }) min = -Infinity;
@property({ type: Number }) max = Infinity;
render() {
return html`
<button @click=${() => this.count > this.min && this.count--}>-</button>
<span>${this.count}</span>
<button @click=${() => this.count < this.max && this.count++}>+</button>
`;
}
}
Using Web Components in Frameworks
React 19 finally ships proper Custom Element support, passing properties (not just attributes) and handling events correctly. Vue and Angular have supported Web Components natively for years. Svelte treats them as standard HTML elements. The interop story is solved.
// React 19
function App() {
return <dw-counter count={5} max={10} onChange={e => console.log(e.detail)} />;
}
// Vue
<template>
<dw-counter :count="5" :max="10" @change="onCountChange" />
</template>
When to Use Web Components
Web Components are ideal for design systems serving multiple teams or frameworks, embeddable widgets (chat, analytics, payment forms), micro-frontend architectures, and any UI that needs to outlive framework churn. They are less ideal when your entire app is a single framework. Use the right tool for the scope.
Further reading: MDN Web Components | Lit Documentation | Custom Elements Everywhere

Leave a Reply