#React#TypeScript

Web Components: Framework-Agnostic Component Standards

React and Vue components are tied to their frameworks. Web Components use browser-native Custom Elements, Shadow DOM, and HTML Templates for cross-framework reuse. This guide explains their APIs, tradeoffs, best use cases, cases to avoid, and how Lit improves the development experience.

Key Points

  • Topic Focus: React、TypeScript
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Web Components: Framework-Agnostic Component Standards

Every frontend developer has experienced this pain: you build a component library in React, the business migrates to Vue, and the entire library must be rewritten. Move to vanilla JavaScript later? Rewrite it again.

The problem is not you; it is the framework. React components belong to React, and Vue components belong to Vue. Their "components" run inside their respective runtimes and stop working when the host changes.

Web Components (WC) offer another path: a browser-native component standard. They do not depend on any framework, are supported natively by Chrome/Safari/Firefox/Edge, and can be written once for use in React, Vue, Angular, and vanilla JavaScript.

This article explains WC's three core APIs, how it fundamentally differs from React/Vue components, when to use it, when to avoid it, and how Google's Lit brings the WC development experience close to React.


1. What Exactly Are Web Components?

In one sentence: Web Components are a browser-native component standard defined by W3C/WHATWG and composed of three native APIs.

They are not a framework, but a set of browser capabilities:

  • customElements.define(): Registers custom HTML elements
  • Element.attachShadow(): Creates an isolated tree for styles and DOM
  • The <template> element: Defines lazy, reusable DOM fragments

All three APIs are built into the browser. You do not need to import a library or use a bundler. When you create <my-button>, it becomes a first-class HTML element at the same level as <button> and <div>.

That is WC's sharpest selling point: it is long-lived and cross-framework. As long as the HTML standard does not remove it, which it will not in the coming decades, your component will continue to run.


2. Breaking Down the Three Core Technologies

1. Custom Elements: Custom HTML Tags

Custom Elements let you register an entirely new HTML tag and define its behavior. They are the heart of WC.

typescript
class MyButton extends HTMLElement {
  connectedCallback() {
    // Called when the element is inserted into the DOM
    this.render();
  }

  // Observe attribute changes (attributes must first be declared in observedAttributes)
  static get observedAttributes() {
    return ['disabled'];
  }

  attributeChangedCallback(name: string, _old: string, newVal: string) {
    if (name === 'disabled') {
      this.classList.toggle('is-disabled', newVal !== null);
    }
  }

  private render() {
    this.innerHTML = `<button>Click me</button>`;
  }
}

// Register the custom element; its tag name must contain a hyphen to avoid conflicts with native elements
customElements.define('my-button', MyButton);

You use it exactly like a native element:

html
<my-button disabled></my-button>

Several key points:

  • The tag name must contain a hyphen (my-button, ui-card). The standard requires this to prevent conflicts with future native elements.
  • Lifecycle: connectedCallback (inserted into the DOM), disconnectedCallback (removed), and attributeChangedCallback (attribute changed) correspond to React's useEffect / useEffect cleanup / dependency changes.
  • Subclassing: You can inherit from a native element (class MyButton extends HTMLButtonElement) and use it as <button is="my-button">.

2. Shadow DOM: An Isolation Boundary for Styles and DOM

Shadow DOM is WC's second major strength. It attaches a "shadow DOM tree" to an element, completely isolating its internal styles and structure from the outside. Internal CSS does not leak out, and external CSS cannot get in.

typescript
class MyCard extends HTMLElement {
  connectedCallback() {
    // Enable Shadow DOM; mode: 'open' allows external access through element.shadowRoot
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        /* These styles apply only inside the Shadow DOM and never pollute the global scope */
        .card {
          padding: 16px;
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          font-family: system-ui;
        }
        ::slotted(*) { /* Style child nodes passed in from the outside */
          color: #374151;
        }
      </style>
      <div class="card">
        <slot></slot>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);
html
<my-card>
  <p>This text is projected into the Shadow DOM through &lt;slot&gt;</p>
</my-card>

<slot> is Shadow DOM's slot mechanism. Like Vue's <slot> or React's children, it "projects" external child nodes into a designated location inside the component.

Shadow DOM provides very concrete practical value:

  • You do not need to worry about BEM namespaces when building a component library. Styles are naturally isolated, so a user's global CSS cannot completely ruin your buttons.
  • Strict encapsulation: External document.querySelector calls cannot enter the Shadow DOM, and internal nodes do not leak into the global scope.

There is also a cost: search engines cannot see Shadow DOM content by default. Use it cautiously on SEO-focused sites, as discussed below.

3. HTML Templates: Reusable, Lazy DOM Templates

The <template> element defines a DOM structure, but the browser does not render it until you clone it and insert the copy into the document. It works particularly well with <slot>.

html
<template id="row-template">
  <tr>
    <td class="name"></td>
    <td class="age"></td>
  </tr>
</template>

<script>
  const tpl = document.getElementById('row-template');
  const row = tpl.content.cloneNode(true); // Deep-clone the template
  row.querySelector('.name').textContent = 'Zhang San';
  row.querySelector('.age').textContent = '28';
  document.querySelector('tbody').appendChild(row);
</script>

The contents of <template> are inert: images do not load, scripts do not execute, and styles do not take effect. This ability to "declare structure without rendering it immediately" makes <template> an excellent template container for complex Web Components.

In practice, more developers use template strings directly (shadowRoot.innerHTML = '...'), so <template> is less common. However, when you need to predefine static structures and clone them dynamically, it remains the cleanest option.


3. The Fundamental Differences from React/Vue Components

Place WC and React/Vue components side by side, and the differences become clear:

DimensionWeb ComponentsReact/Vue Components
Runtime dependencyNative to the browser, zero dependenciesRequires the corresponding framework runtime
Cross-framework reuseOne component works with every frameworkChanging frameworks means rewriting it
LifecycleStable browser standardFollows framework versions and may have breaking changes
Templates/reactivityNot provided natively; build it yourself or use LitDeclarative templates + reactive data out of the box
EcosystemRelatively smallThe React ecosystem is unmatched
SSR/SEOShadow DOM is not friendly to eitherMature solutions (Next/Nuxt)

In one sentence: React/Vue components are "components inside a framework," while WC are "components inside HTML."

A React component is essentially a function that returns virtual DOM. It must run on the react-dom runtime; outside React, it is inert code. A WC is a real HTML element at the same level as <div>. Any environment that can manipulate the DOM, including JavaScript, React, Vue, Angular, jQuery, or vanilla code, can use <my-button /> directly.

tsx
// Use WC directly in a React project with zero adaptation
function App() {
  return <my-button onClick={handleClick}>Button</my-button>;
}

This ability for a component to outlive its framework is WC's unique and irreplaceable value.


4. WC's Advantages: Why People Choose It

  1. Truly cross-framework. One design system can serve React, Vue, Angular, and vanilla projects. This is WC's strongest use case. Design systems from major organizations, including Ionic, SAP UI5, Adobe Spectrum, and some GitHub components, use WC for this reason.

  2. It lasts. Once browsers implement a standard, it is almost never removed. WC written ten years ago still runs today. What about code written with Backbone ten years ago? Even React Class components are now considered "supported but not recommended."

  3. Natural style isolation. Shadow DOM eliminates the need to worry about CSS namespaces. You can build a clean component library without Tailwind.

  4. Simple distribution. A WC is just a JavaScript file. Include it with <script> and it works, without requiring users to npm install a pile of dependencies. This makes it suitable for embedded components distributed to third parties.


5. WC's Disadvantages: Do Not Treat It as a Silver Bullet

  1. The API is low-level, and the development experience is poor. Native WC provides neither declarative templates nor reactive data. To build a form component, you must manually observe attribute changes and call this.shadowRoot.querySelector to update the DOM. That feels like development in the jQuery era. Real projects almost always need a library such as Lit.

  2. SEO is a major weakness. Shadow DOM content is not indexed by search engines by default. Although this has improved in recent years, it remains unreliable. WC is not a good choice for content sites that need SSR and depend heavily on SEO.

  3. Form integration is cumbersome. Native WC elements do not participate in <form> validation by default. You must do substantial work with the ElementInternals API before a custom element behaves like <input>. The mental overhead is significant.

  4. The ecosystem is far smaller than React/Vue's. React has more than 200,000 npm packages, while the WC ecosystem is much smaller. The library you need may not exist, and if it does, it may not be well maintained.

  5. TypeScript support is mediocre. When WC tags are used in JSX, TypeScript reports type errors by default, so you must declare JSX.IntrinsicElements yourself.


6. When WC Is Worth Using

In practice, I recommend WC for these scenarios:

  1. Cross-framework component libraries / design systems. This is WC's ideal use case. If a company has multiple technology stacks, such as a legacy jQuery project, an internal React platform, and new Vue applications, a design system built once with WC can serve all of them without maintaining three versions.

  2. Micro-frontends. Encapsulate a complex module as a WC and embed it anywhere in any host application. Framework independence and style isolation make it a natural fit for integration across teams and technology stacks.

  3. Browser extensions. An extension's content script is injected into third-party pages. WC's Shadow DOM prevents the host page from affecting your UI styles and prevents your UI from affecting the host.

  4. Embedded components distributed to third parties (comment boxes, players, payment buttons). A single <script> element is enough, and consumers do not need to install numerous dependencies.


7. When Not to Use WC

Conversely, WC creates unnecessary problems in these scenarios:

  1. Business applications built with a single framework. If the company has only one internal React platform, use React components for business UI. Reactivity, the ecosystem, and SSR are already available. Adopting WC in advance because you "might change frameworks someday" is overengineering.

  2. SEO-heavy content sites. Blogs, e-commerce product pages, and news sites depend on search traffic, while search engines cannot crawl Shadow DOM content reliably. SSR with Next.js/Nuxt is the correct solution.

  3. Admin systems that depend heavily on forms and validation. Native WC form integration carries substantial mental overhead. React/Vue form libraries such as Formik, react-hook-form, and VeeValidate are much easier to use.

  4. Small teams focused on rapid iteration. Even with Lit, WC's development experience cannot match the speed of React + Tailwind.


8. Lit: Moving WC Beyond Low-Level APIs

As noted repeatedly above, native WC has a poor development experience. The industry's solution is Lit, built by Google. It is a lightweight library (about 5KB) that adds declarative templates and reactive properties on top of native WC, bringing the development experience close to React.

Here is the same button implemented with Lit:

typescript
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-button') // Calls customElements.define automatically
export class MyButton extends LitElement {
  // Reactive properties: changes automatically trigger a re-render
  @property({ type: Boolean }) disabled = false;
  @property() label = 'Click me';

  static styles = css`
    button {
      padding: 8px 16px;
      border-radius: 6px;
      background: #2563eb;
      color: white;
      cursor: pointer;
    }
    button.is-disabled {
      background: #9ca3af;
      cursor: not-allowed;
    }
  `;

  render() {
    return html`
      <button class=${this.disabled ? 'is-disabled' : ''} ?disabled=${this.disabled}>
        ${this.label}
      </button>
    `;
  }
}

Compared with the native version, Lit's benefits are immediately apparent:

  • Declarative templates: html template strings eliminate manual innerHTML and querySelector calls.
  • Reactive properties: Changes to properties marked with @property automatically trigger targeted re-renders, similar to React state.
  • Automatic style scoping: static styles is injected into the Shadow DOM automatically, so you do not need to call attachShadow manually.
  • Decorator-based registration: @customElement('my-button') handles registration in one line.

Lit still outputs standard WC, so none of the cross-framework, long-lived, or distributable qualities are lost. Conclusion: if you choose WC, production projects should almost always pair it with Lit instead of writing everything with native APIs.


Summary

  • Web Components are a browser-native component standard (Custom Elements + Shadow DOM + HTML Templates) with zero dependencies, cross-framework compatibility, and long-term stability.
  • Cross-framework compatibility, longevity, and style isolation are their three major advantages; low-level APIs, difficult SEO, and a small ecosystem are their major weaknesses.
  • Good use cases: cross-framework component libraries/design systems, micro-frontends, browser extensions, and embedded components distributed to third parties.
  • Poor use cases: single-framework business applications, SEO-heavy content sites, form-heavy admin systems, and small teams focused on rapid iteration.
  • Use Lit in production. Its declarative and reactive model brings the native WC development experience close to React.

Key takeaway: frameworks become obsolete; standards do not. Web Components are not meant to replace React. They are meant for components that "outlive the framework."

Related

Related Content