AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 29 — Web Components — Reusable Elements the Platform Already Ships

29 — Web Components — Reusable Elements the Platform Already Ships

August 12, 20268 min read
Download as Markdown

"A React component, but without React" was my Web Components assumption, and it hid the platform mechanics. The model that stuck: a Web Component isn't one feature, it's three platform features composed into a single self-contained element. [1] Once I could see the three pieces separately, the appeal clicked — a component that works in React, Vue, plain HTML, or nothing at all, because it isn't a library abstraction. It's a browser element, the same kind as <button>.

The three pieces are Custom Elements (define your own tag and wire behavior to it), Shadow DOM (a private, isolated subtree of the DOM — the browser's in-memory tree of the page — so the component's markup and styles can't leak out, or be reached from outside), and HTML <template> (an inert blueprint you clone instead of parsing markup by hand). None of them is a framework. They're shipped by the browser. Here's how they stack into one component:

HTML <template> an inert blueprint — clone it, don't parse it Custom Element a JS class that defines the tag + behavior Shadow DOM isolated subtree — styles and DOM stay inside <my-button> one self-contained element three platform features — one reusable element, any framework (or none)

The payoff of seeing them separately is that each piece solves a distinct problem the others don't, and I no longer conflated them. I'll take them one at a time, then show where they meet.

Custom Elements: defining your own tag

The first piece is the one I'd been faking for years with a class="button" div. A Custom Element lets me define a brand-new HTML tag with its own behavior, registered with the browser so it's treated like any built-in element [2].

The mechanics are small: write a JavaScript class extending HTMLElement, then call customElements.define('my-tag', MyTag). The hyphen in the name is mandatory — it's how the browser tells custom tags apart from future built-ins. The class gets lifecycle callbacks — connectedCallback fires when the element enters the DOM, disconnectedCallback when it leaves, attributeChangedCallback when a watched attribute changes — which is the same idea as React's mount/unmount/update lifecycle, but as a native browser hook, not a library abstraction.

The shift in framing: I'm not building a component _that the framework renders._ I'm extending the browser's own element vocabulary. Once defined, <my-button> is just a tag — it works in plain HTML, in a Vue template, inside React JSX, or rendered by a server. That portability is the whole reason the spec exists.

Shadow DOM: the isolation boundary

The second piece is what makes a component actually _self-contained_ — and it's the one that took longest to click, because it's invisible. Shadow DOM lets an element host a private DOM subtree that's sealed off from the main document [3]. The page can't see in with its CSS; the component's styles can't leak out.

The thing I had to straighten out: Shadow DOM isn't a visual thing. It's a DOM _boundary._ Styles in the main page's stylesheet don't reach the shadow tree (so a global button { color: red } won't repaint my component's buttons), and styles defined inside the shadow tree don't escape (so my component's .active class won't accidentally style the rest of the page). Before this, "component CSS leaks" was a problem I solved with naming conventions — BEM (a naming scheme), CSS Modules, scoped selectors. Shadow DOM solves it at the platform level: the boundary just won't let styles cross it [4].

The same isolation applies to the DOM itself. document.querySelectorAll('div') won't find divs living inside a shadow tree — you have to pierce into it deliberately. That's why this isn't "another div on the page." It's a sealed subtree. For something like a date picker or a modal that shouldn't care what page it lands on, that seal is the entire value proposition.

HTML Templates: the inert blueprint

The third piece is the one I'd been doing the dumb way. HTML <template> is an element whose content is parsed but not rendered — a reusable chunk of markup held in reserve, waiting to be cloned [5]. Inside a <template>, <img> tags don't load, <script> tags don't run, nested elements don't exist on the page. It's pure structure, parked.

The payoff is in the word _cloned._ When my Custom Element needs to populate its shadow root, I reach for template.content.cloneNode(true) instead of concatenating HTML strings or hand-building nodes with createElement. The browser already parsed the markup once; cloning is cheap, and there's no risk of accidentally triggering network requests or scripts the way innerHTML can. For a component rendered many times, that blueprint-then-clone pattern is the efficient path.

<template> also pairs with slots — <slot name="header"> — which are the contract for letting the outside page inject content into the component's shadow tree [5]. Slots are how a component stays encapsulated (wrapped up so the outside can't reach in) while still being composable: the markup lives in the page, lands in the right slot, and the component decides where it renders.

Where the three meet

The composition is what makes it worth writing the notes down. None of the three features alone is a component:

  • Custom Elements alone give you a tag with behavior, but its markup still lives in the open DOM — styles leak, selectors collide.
  • Shadow DOM alone gives you isolation, but you'd be filling it by hand every time.
  • <template> alone is a handy markup stash, but it doesn't define an element or isolate anything.

Stacked, they cover each other's gaps. The Custom Element class, in its connectedCallback, attaches a shadow root and clones a <template> into it [1]. From outside, the page sees a single tag with attributes and slots; from inside, the component sees its own sealed DOM with its own styles. That's a self-contained element — reusable across projects and frameworks because nothing about it depends on any framework's runtime.

How I use this

The practical note I keep is a rule of thumb, not a mandate. Web Components aren't a replacement for React or Vue in my stack — those still do the app-level job well. Where the model pays off is at the edges: a self-contained widget (a date picker, a video player, a design-system button) that has to survive being dropped into pages I don't control — different frameworks, different global stylesheets, different teams. For that, a Custom Element with a shadow root is the one artifact that won't bleed or be bled on.

The habit these notes left me with is to ask, before reaching for a framework component, one question: _does this need to work outside my framework?_ If yes, the browser already ships the three pieces I need. If no, a framework component is fine — but it's worth knowing the native option exists, because the whole point of Web Components is that it's there whether I use a library or not.

References

[1] Mozilla, "Web Components," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/Web_Components

[2] Mozilla, "Using custom elements," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements

[3] Mozilla, "Using shadow DOM," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

[4] "Web Components," webcomponents.org, 2024. [Online]. Available: https://webcomponents.org/

[5] Mozilla, "Using templates and slots," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots

Knowledge check · Question 1 of 5

A Web Component is best described as…

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!