
Photo by JUNHØ on Unsplash
Demystifying Web Components in JavaScript: The Native Way to Build Reusable UI
In the world of frontend development, we’ve been spoiled by frameworks — React, Vue, Angular — all offering component-driven architectures that simplify UI development. But beneath all that magic lies something native and powerful: Web Components.
They're built into the browser, require no build tools, and can bring real modularity and encapsulation — all without tying you to a framework.
If you're a JavaScript developer looking to create highly reusable, framework-agnostic UI components, this post is your practical deep dive into the what, why, and how of Web Components.
🧩 What Are Web Components?
Web Components are a set of native browser APIs that let you define custom HTML elements with their own structure, behavior, and style — all encapsulated and reusable.
They consist of four core technologies:
-
Custom Elements — Define custom HTML tags with your own behavior.
-
Shadow DOM — Isolate your component's internal DOM and styles.
-
HTML Templates — Define chunks of HTML for reuse without rendering them immediately.
-
ES Modules — (Often used) to load JS modules in a clean, modular way.
Together, they allow you to write components like:
<user-card name="John" role="Software Developer"></user-card>
...and implement the behavior without React, Vue, or even jQuery.
⚙️ Building Blocks of Web Components
Let’s explore the core APIs individually:
1. Custom Elements
You can define your own HTML tags and associate behavior with them via JavaScript.
class MyButton extends HTMLElement { connectedCallback() { this.innerHTML = `<button>Click me</button>`; } } customElements.define('my-button', MyButton);
Usage:
<my-button></my-button>
The lifecycle methods available:
-
constructor()
: Runs when the element is created. -
connectedCallback()
: Runs when added to the DOM. -
disconnectedCallback()
: Runs when removed. -
attributeChangedCallback(name, oldValue, newValue)
: Runs when attributes change.
You can also specify which attributes to observe:
static get observedAttributes() { return ['label']; }
2. Shadow DOM
Shadow DOM allows you to encapsulate markup and styles so they don’t leak into or out of your component.
class ShadowGreeting extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> p { color: red; font-weight: bold; } </style> <p>Hello from the Shadow DOM!</p> `; } } customElements.define('shadow-greeting', ShadowGreeting);
Why it's useful:
-
Prevents style leaks (no global CSS pollution).
-
Enables true component encapsulation.
3. HTML Templates
The <template>
tag lets you define reusable markup without rendering it until you need it.
<template id="user-template"> <div class="user"> <p><slot name="name"></slot></p> </div> </template>
And in JS:
class UserCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const template = document.getElementById('user-template').content.cloneNode(true); shadow.appendChild(template); } } customElements.define('user-card', UserCard);
This improves performance and keeps things clean.
🧠 A Practical Example: <fancy-card>
Let’s build a real component step by step.
class FancyCard extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const wrapper = document.createElement('div'); wrapper.innerHTML = ` <style> .card { padding: 1rem; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-radius: 8px; font-family: sans-serif; } .title { font-size: 1.25rem; margin-bottom: 0.5rem; } </style> <div class="card"> <div class="title"><slot name="title"></slot></div> <div class="content"><slot></slot></div> </div> `; shadow.appendChild(wrapper); } } customElements.define('fancy-card', FancyCard);
Use it like this:
<fancy-card> <span slot="title">Welcome!</span> <p>This is a beautiful web component.</p> </fancy-card>
⚡ Benefits of Web Components
Feature | Description |
---|---|
🔄 Reusable | Define once, reuse everywhere |
🧩 Encapsulated | CSS and DOM are scoped |
🚫 No Framework Lock-in | Vanilla JS or works within any framework |
🔌 Interop Friendly | Use in React, Angular, Vue apps |
🔒 Stable | Part of the web standard, future-proof |
🛠️ Real-World Use Cases
-
Design systems used by teams across multiple projects (e.g., Google’s Material Web Components).
-
E-commerce widgets like product cards or carousels.
-
Micro frontends, where individual apps/components are shipped by different teams.
-
Embeddable widgets (e.g., feedback forms, newsletter popups) for third-party websites.
❗ Pitfalls & Considerations
While Web Components are powerful, they’re not always the best fit for everything.
❌ Verbose API — Writing in pure JS feels clunky compared to JSX or templates.
❌ State Management — No built-in reactive data flow (yet).
❌ Tooling Ecosystem — Less mature than React/Vue.
❌ SEO — Content in Shadow DOM may not be indexed (depending on crawler).
🧪 Want More Power? Use a Library
If you like the idea of Web Components but hate the boilerplate, use a modern library:
🔥 Lit
-
Developed by Google
-
Brings declarative templates to Web Components
-
Example:
import { LitElement, html, css } from 'lit'; class MyLitCard extends LitElement { static styles = css` .card { padding: 1rem; border: 1px solid #ccc; } `; render() { return html`<div class="card"><slot></slot></div>`; } } customElements.define('my-lit-card', MyLitCard);
🛠️ Other Options:
-
Stencil (from the Ionic team)
-
Hybrids, SkateJS
-
Shoelace – prebuilt accessible UI components
🧘♂️ Final Thoughts
Web Components give you freedom — to build once, deploy anywhere, and stop rebuilding the same button in every framework.
They’re:
-
Lightweight
-
Fast
-
Natively supported
-
Easy to share and distribute
If you’re building UI libraries, micro frontends, or embeddable widgets — Web Components should be in your toolbox.
📦 TL;DR
-
Web Components are native to the browser.
-
They consist of: Custom Elements, Shadow DOM, Templates.
-
Great for reusable UI with no framework dependency.
-
Lit makes them easier to work with.
-
Use them when you want portability, isolation, and longevity.