Skip to the content.

Web components are the platform's answer to reusable UI

Web components are the platform's answer to reusable UI

Web components are the browser’s own answer to “reusable UI component,” and they predate needing a framework for it. Three platform features combine: custom elements (define your own HTML tag with behaviour), shadow DOM (encapsulate its markup and styles so nothing leaks in or out), and templates (declare reusable markup). Together they let you ship a <my-widget> that any page — React, Vue, Angular, or plain HTML — can use, because the browser understands it natively. They will not replace your framework for building a whole app, but for cross-framework, long-lived UI — a design system consumed by many teams on many stacks — they are exactly the right tool, precisely because they belong to the platform and not to a framework that will change.

One custom element with encapsulated shadow DOM works in any framework A single custom element defined once, consumed unchanged by a React app, a Vue app, and a plain HTML page, with its shadow DOM encapsulating its internals. <my-button>custom element + shadow DOM React app Vue app plain HTML
Define the element once; every framework consumes the same tag. Its shadow DOM seals the internals so no consumer's CSS leaks in and its styles don't leak out.

A custom element is a tag the browser understands

You extend HTMLElement, define lifecycle callbacks, and register a tag name. From then on <my-button> works in any HTML, no import, no framework:

class MyButton extends HTMLElement {
  connectedCallback() {                    // lifecycle: when it enters the DOM
    const shadow = this.attachShadow({ mode: "open" });   // encapsulation boundary
    shadow.innerHTML = `
      <style>button { background: var(--brand, #fe854c); }</style>
      <button><slot></slot></button>`;     // <slot> projects the consumer's content
  }
}
customElements.define("my-button", MyButton);   // now <my-button> is a real tag

The shadow DOM is the key move: styles inside it do not leak out, and the page’s CSS does not leak in, so the component looks the same wherever it is dropped.

Style seams: parts and custom properties

Encapsulation would be a prison if consumers could not theme the component, so the platform provides deliberate seams. var(--brand) above lets a consumer set a custom property that pierces the shadow boundary; ::part() lets them style specific internals you expose:

/* the consuming page themes the encapsulated component through the seams you allow */
my-button { --brand: #157878; }          /* custom property crosses the boundary */
my-button::part(label) { font-weight: 700; }   /* style a part the element exposed */

You choose exactly what is themeable — encapsulation by default, customization by opt-in.

Where web components fit, and where they don’t

Be honest about the trade. Web components are the right tool for a design system or widget shared across frameworks and meant to outlive any one of them — a component library a big org’s many stacks all consume, an embeddable widget, anything that must work in a page you do not control. They are a poor fit for building a whole application’s view layer: they lack the ergonomics, state management, and rich ecosystem a framework gives, and reinventing those on top of raw custom elements is a lot of work (which is why Lit and similar exist to smooth it). So the pattern is usually web components for the shared, long-lived, cross-framework pieces; a framework for the app that assembles them. Their superpower is belonging to the platform, which means they cannot be deprecated by a framework’s next major version. The theme-toggle and loading-button-atom exercises are natural web-component shapes — small, reusable, themeable through the seams — and good places to feel where the platform’s own components shine.