Understanding Web Components: Core Concepts, Custom Elements, Shadow DOM, Slots, and Templates
This article introduces Web Components—a W3C‑standardized technology supported by all major browsers—explaining its core concepts (Custom Elements, Shadow DOM, Templates, Slots), providing step‑by‑step code examples, discussing lifecycle callbacks, compatibility, polyfills, and how it compares with frameworks like React and Vue.
The rapid evolution of web technologies has produced many new concepts and frameworks, especially in frontend development; Vue 3 and Vite are popular, but the older yet powerful Web Components deserve attention because they enable framework‑agnostic, reusable custom elements.
Web Components are a set of W3C‑standardized APIs (Custom Elements, Shadow DOM, HTML Templates, Slots) that are now natively supported by Chrome, Firefox, Safari, and Edge. Vue 3 even provides native support for Web Components, and many interview questions now focus on Shadow DOM.
Web Components Core Concepts
Web Components allow developers to create reusable custom elements that work in any browser supporting the standard.
Key concepts include:
Custom Elements : define new HTML tags with custom behavior and styling.
Shadow DOM : encapsulate a component’s internal structure and styles, preventing global namespace pollution.
Templates : define reusable HTML structures that can be instantiated multiple times.
Slots : placeholders that let external content be inserted into a component.
The following sections demonstrate how to build reusable custom elements using these concepts.
Custom Elements (自定义元素)
Custom Elements let you package HTML into a new tag. Below is a simple button example.
Creating a Custom Element
First, define the element with customElements.define() and give it a name such as my-button.
<code class="language-javascript">// main.js
class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
button {
background-color: #4CAF50;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
padding: 10px 24px;
border-radius: 4px;
}
</style>
<button>Click Me!</button>
`;
}
}
customElements.define('my-button', MyButton);
</code>Use the element directly in HTML:
<code class="language-html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Components Example</title>
</head>
<body>
<my-button></my-button>
<script src="./main.js"></script>
</body>
</html>
</code>The button renders with the defined style. Custom Elements also support behavior such as event listeners.
<code class="language-javascript">const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.querySelector('button').addEventListener('click', () => {
alert('Button clicked!');
});
</code>Lifecycle Callback Methods
Custom Elements provide lifecycle callbacks that are invoked at different stages of an element’s existence.
constructor() : called when the element instance is created; good for initializing state and attaching Shadow DOM.
connectedCallback() : called when the element is inserted into the DOM; useful for fetching data or rendering content.
disconnectedCallback() : called when the element is removed from the DOM; useful for cleanup.
attributeChangedCallback(name, oldValue, newValue) : called when observed attributes change; requires a static observedAttributes array.
Example implementation:
<code class="language-javascript">class MyCustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<p>This is a custom element</p>';
}
connectedCallback() {
console.log('Custom element connected to the DOM');
}
disconnectedCallback() {
console.log('Custom element disconnected from the DOM');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
static get observedAttributes() { return ['my-attribute']; }
}
customElements.define('my-custom-element', MyCustomElement);
</code>Use it in HTML with an observed attribute:
<code class="language-html"><my-custom-element my-attribute="value"></my-custom-element>
</code>Shadow DOM (影子 DOM)
Shadow DOM creates an isolated DOM subtree whose styles and markup do not affect the main document and vice versa.
Using innerHTML
<code class="language-javascript">class MyElementInnerHTML extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>p { color: black; }</style>
<p>Using innerHTML</p>
`;
}
}
customElements.define('my-element-inner', MyElementInnerHTML);
</code>Using createElement and appendChild
<code class="language-javascript">const wrapper = document.createElement('p');
wrapper.textContent = 'Using createElement and appendChild';
const style = document.createElement('style');
style.textContent = `p { color: gray; }`;
class MyElementAppend extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(wrapper);
shadowRoot.appendChild(style);
}
}
customElements.define('my-element-append', MyElementAppend);
</code>Shadow Mode
When creating a Shadow DOM you can specify mode: 'open' or mode: 'closed'. In open mode the shadow root is accessible via element.shadowRoot; in closed mode it returns null.
<code class="language-javascript">class OpenMyElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>p { color: red; }</style>
<p>This is an open mode Shadow DOM</p>
`;
}
}
customElements.define('open-my-element', OpenMyElement);
// Access from outside
const el = document.querySelector('open-my-element');
console.log(el.shadowRoot); // ShadowRoot object
</code> <code class="language-javascript">class ClosedMyElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.innerHTML = `
<style>p { color: blue; }</style>
<p>This is a closed mode Shadow DOM</p>
`;
}
}
customElements.define('closed-my-element', ClosedMyElement);
// Attempt to access
const el = document.querySelector('closed-my-element');
console.log(el.shadowRoot); // null
</code>Slots (插槽)
Slots allow external content to be projected into a component, increasing flexibility.
Basic Slot Usage
<code class="language-javascript">class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>/* ...styles... */</style>
<button><slot>Click Me!</slot></button>
`;
}
}
customElements.define('my-button', MyButton);
</code>Now any content placed inside <my-button> replaces the default slot:
<code class="language-html"><my-button>Custom Text</my-button>
</code>Named Slots
<code class="language-javascript">class MyButtonName extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>/* ...styles... */</style>
<button>
<slot name="element-name"></slot>
<slot name="element-age"></slot>
<slot name="element-email"></slot>
</button>
`;
}
}
customElements.define('my-button-name', MyButtonName);
</code> <code class="language-html"><my-button-name>
<span slot="element-name">John Doe</span>
</my-button-name>
<my-button-name>
<span slot="element-age">30</span>
</my-button-name>
<my-button-name>
<span slot="element-email">[email protected]</span>
</my-button-name>
</code>Templates (模板)
Templates let you define reusable HTML fragments that can be cloned into a Shadow DOM.
<code class="language-html"><my-button></my-button>
<template id="my-button-template">
<style>/* ...styles... */</style>
<button><slot>Click Me!</slot></button>
</template>
</code> <code class="language-javascript">class MyButton extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
const template = document.getElementById('my-button-template');
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('my-button', MyButton);
</code>Related Extensions
Compatibility
Most modern browsers now natively support Custom Elements, Shadow DOM, and HTML Templates, but older browsers may require polyfills.
Polyfills
Use @webcomponents/webcomponentsjs to add support for browsers that lack native implementations.
<code class="language-bash">npm install @webcomponents/webcomponentsjs
</code> <code class="language-html"><!-- load webcomponents bundle -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<!-- load the custom element -->
<script type="module" src="my-element.js"></script>
<!-- use the element -->
<my-element></my-element>
</code>Comparison with React and Vue
Web Components are framework‑agnostic, lightweight, and provide true encapsulation via Shadow DOM, making them ideal for component libraries. However, they lack the rich ecosystem, state‑management, routing, and performance optimizations (e.g., virtual DOM) that frameworks like React and Vue offer.
Real‑World Use Cases
Vue 3 can compile Vue components to Web Components.
MicroApp – a micro‑frontend framework built on Web Components.
Twitter switched embedded tweets from iframes to Shadow DOM for better performance.
Svelte + Vite can quickly scaffold Web Component projects.
Polymer provides tooling to simplify custom element creation.
References
MDN Web Docs – Web Components Introduction
"You Don't Know Web Components" – current status
Custom Elements v1 – reusable web components
Web Components Tutorial for Beginners (2019)
Conclusion
Web Components, driven by the W3C, enable developers to create standard, reusable UI elements with encapsulated markup, style, and behavior. Their adoption across many projects demonstrates their value, and understanding them helps bridge the gap between framework‑specific components and native web standards.
Hope this article helps you; feel free to discuss in the comments.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
