Articles

What Are Web Components? Custom Elements Explained

Web Components are browser-native APIs for building reusable, encapsulated custom elements that work in any framework, or none at all.

Takina Takina · · 4 min read
Close-up of a code editor showing component markup

Web Components are a set of browser-native APIs — custom elements, shadow DOM, and HTML templates — that let you build reusable, encapsulated UI elements without a framework. A well-built web component behaves like a native HTML tag: drop <my-widget> into any page, in any framework or none at all, and it renders and manages its own state without leaking styles or markup into the surrounding document.

Unlike a React or Svelte component, a web component isn’t compiled away into framework-specific output — it’s a real, standards-based DOM element that the browser understands natively.

The three specifications

Web Components is really three separate browser specs working together:

  • Custom Elements — the API for defining a new HTML tag and its behavior. You extend HTMLElement, register the class with customElements.define("my-widget", MyWidget), and the browser calls lifecycle hooks like connectedCallback (mounted), disconnectedCallback (removed), and attributeChangedCallback (an observed attribute changed) automatically.
  • Shadow DOM — an encapsulated DOM subtree attached to an element via element.attachShadow({ mode: "open" }). Styles defined inside a shadow root don’t leak out, and page styles don’t leak in — the classic cascade problem in large CSS codebases simply doesn’t apply across a shadow boundary.
  • HTML Templates — the <template> and <slot> elements. Content inside <template> is inert (not rendered, scripts don’t run) until it’s cloned into the DOM at runtime, and <slot> lets a component’s shadow DOM accept light-DOM children from whoever uses it, similar to a framework’s children prop.

A minimal example

class HelloBadge extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: "open" });
    shadow.innerHTML = `
      <style>span { color: rebeccapurple; font-weight: 600; }</style>
      <span>Hello, <slot></slot></span>
    `;
  }
}
customElements.define("hello-badge", HelloBadge);

Used as <hello-badge>world</hello-badge>, the browser renders it immediately — no build step, no runtime library, no virtual DOM diffing. The styles are scoped to the shadow root and cannot be overridden by an ancestor’s stylesheet, which is a meaningfully different guarantee than what a class-based CSS convention like BEM offers.

Why encapsulation matters

In a large codebase, unscoped CSS is a constant source of accidental collisions — a .card class defined for one feature quietly overrides another team’s .card. Shadow DOM makes that class of bug structurally impossible: styles simply cannot cross the boundary in either direction. The one sanctioned way to theme through a shadow boundary is CSS custom properties, which are designed to pierce shadow roots by inheritance so a host page can still set a component’s color or spacing tokens.

This is a different tradeoff than component-based frameworks make. React’s virtual DOM and CSS Modules solve style and update collisions at build time and convention level; web components solve them at the browser’s DOM layer, which is why a component built this way survives being dropped into a codebase that has nothing to do with how it was authored.

Web components vs framework components

Web ComponentsFramework components (React, Vue, Svelte)
Runtime dependencyNone — native browser APIsRequires the framework’s runtime (except compiled frameworks like Svelte, which still need some glue)
Style isolationTrue encapsulation via shadow DOMConvention or build-tool enforced (CSS Modules, scoped styles)
InteropWorks in any framework, or noneFramework-specific; wrapping into another framework needs adapters
EcosystemSmaller — fewer batteries-included patternsLarge — routing, state management, dev tools all mature
Best forDesign systems, embeddable widgets shared across teams/frameworksApplication UI within a single framework’s codebase

Frameworks like Svelte or React can render into a page and interoperate with custom elements reasonably well, but round-tripping framework-specific state (like passing complex objects as props, or subscribing to a component’s internal reactivity) across a custom element boundary is where the friction shows up.

Where they fit today

Web components tend to earn their keep in a specific niche: UI that needs to work across framework boundaries. A design system built as native custom elements can be consumed identically by a legacy jQuery app, a React app, and a Vue app, because none of them need to understand each other’s internals — they just see an HTML tag. Third-party embeddable widgets (chat bubbles, payment forms, video players) are another natural fit, since the embedding site’s framework is unknown and unencapsulated CSS would be a liability.

For a typical single-framework application, though, the framework’s own component model is usually the more productive choice — it comes with better dev tooling, state management, and community conventions than the more low-level web component APIs offer on their own.

The takeaway

Web Components give you framework-agnostic, browser-native encapsulation through three complementary specs: custom elements for defining new tags, shadow DOM for style and markup isolation, and templates for inert, cloneable markup. They shine when a component needs to survive outside the codebase that built it — design systems and embeddable widgets — and are usually overkill for ordinary application UI, where a framework’s component model still wins on developer experience.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend