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.
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 withcustomElements.define("my-widget", MyWidget), and the browser calls lifecycle hooks likeconnectedCallback(mounted),disconnectedCallback(removed), andattributeChangedCallback(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’schildrenprop.
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 Components | Framework components (React, Vue, Svelte) | |
|---|---|---|
| Runtime dependency | None — native browser APIs | Requires the framework’s runtime (except compiled frameworks like Svelte, which still need some glue) |
| Style isolation | True encapsulation via shadow DOM | Convention or build-tool enforced (CSS Modules, scoped styles) |
| Interop | Works in any framework, or none | Framework-specific; wrapping into another framework needs adapters |
| Ecosystem | Smaller — fewer batteries-included patterns | Large — routing, state management, dev tools all mature |
| Best for | Design systems, embeddable widgets shared across teams/frameworks | Application 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.
Tagged
Keep reading
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.
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.
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.