Web Components Deep Dive: Shadow DOM, Custom Elements, and Native Browser Componentization
前端工程(Updated Jun 1, 2026)
Web Components: The Browser-Native Component Model
Web Components are a W3C standard for native browser componentization, built on three core APIs:
| API | Purpose | Status |
|---|---|---|
| Custom Elements | Define new HTML tags | Stable, full browser support |
| Shadow DOM | Encapsulate styles and DOM | Stable, full browser support |
| HTML Templates | Declare reusable DOM fragments | Stable, full browser support |
CSS Parts (
::part()) is a companion API for piercing Shadow DOM styling.
Custom Elements: Define Your HTML Tags
Lifecycle Callbacks
class MyTooltip extends HTMLElement {
static get observedAttributes() {
return ['position', 'content'];
}
constructor() {
super();
console.log('1. constructor — initialization');
}
connectedCallback() {
console.log('2. connectedCallback — inserted into DOM');
this.render();
}
disconnectedCallback() {
console.log('3. disconnectedCallback — removed from DOM');
this.cleanup();
}
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
console.log(`4. attributeChanged: ${name} ${oldVal} → ${newVal}`);
if (oldVal !== newVal) this.update(name, newVal);
}
adoptedCallback() {
console.log('5. adoptedCallback — moved to new document');
}
}
customElements.define('my-tooltip', MyTooltip);
Registration Rules
customElements.define('my-button', MyButton); // ✅
customElements.define('button', MyButton); // ❌ Not allowed
customElements.define('my-button', MyButton); // ✅
customElements.define('my-button', MyButton2); // ❌ Already registered
const MyBtn = customElements.get('my-button');
const defined = customElements.whenDefined('my-button');
defined.then(() => console.log('my-button is ready'));
Shadow DOM: Style and Structure Encapsulation
Open vs. Closed Mode
class ShadowComponent extends HTMLElement {
constructor() {
super();
// Open: external code can access element.shadowRoot
this.attachShadow({ mode: 'open' });
// Closed: external code gets null from element.shadowRoot
// this.attachShadow({ mode: 'closed' });
}
}
| Feature | open | closed |
|---|---|---|
element.shadowRoot |
Returns Shadow Root | Returns null |
| External query inside | Yes | No |
querySelector piercing |
Yes | No |
| Usage in practice | High | Very low |
Style Encapsulation in Practice
class StyledCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
:host {
display: block;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
:host([highlighted]) {
border: 2px solid #4f46e5;
}
::slotted(h2) {
margin: 0;
padding: 16px;
color: #1f2937;
}
.content { padding: 0 16px 16px; }
</style>
<slot name="header"></slot>
<div class="content"><slot></slot></div>
`;
}
}
customElements.define('styled-card', StyledCard);
Usage:
<styled-card highlighted>
<h2 slot="header">Card Title</h2>
<p>Card content with fully encapsulated styles</p>
</styled-card>
CSS Parts: Piercing Shadow DOM Customization
shadow.innerHTML = `
<style>.title { font-size: 18px; }</style>
<h2 class="title" part="title"><slot></slot></h2>
`;
styled-card::part(title) {
font-size: 24px;
color: #e11d48;
}
HTML Templates and Slots
Template: Declarative DOM Fragments
<template id="card-template">
<style>
.card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }
</style>
<div class="card">
<slot name="title"></slot>
<slot></slot>
</div>
</template>
class TemplateCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template') as HTMLTemplateElement;
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('template-card', TemplateCard);
Comparison with Framework Components
| Dimension | Web Components | React | Vue |
|---|---|---|---|
| Standard | W3C standard | Community ecosystem | Community ecosystem |
| Style isolation | Shadow DOM (native) | CSS Modules / CSS-in-JS | Scoped CSS |
| Cross-framework reuse | Native support | Needs wrapper | Needs wrapper |
| Reactivity | Manual (attributeChangedCallback) | Auto (setState/hooks) | Auto (ref/reactive) |
| Templates | String / Template | JSX | SFC Template |
| Ecosystem/tooling | Limited | Very rich | Rich |
| SSR | Limited support | Full support | Full support |
| DX | Low-level | High-level abstraction | High-level abstraction |
Practice: Building a Star Rating Component
class StarRating extends HTMLElement {
private _value = 0;
private _max = 5;
static get observedAttributes() {
return ['value', 'max'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.setupEvents();
}
attributeChangedCallback(name: string, _: string, newVal: string) {
if (name === 'value') this._value = Number(newVal);
if (name === 'max') this._max = Number(newVal);
if (this.shadowRoot) this.render();
}
private render() {
const stars = Array.from({ length: this._max }, (_, i) => {
const filled = i < this._value;
return `<span class="star ${filled ? 'filled' : ''}" data-index="${i}">★</span>`;
}).join('');
this.shadowRoot!.innerHTML = `
<style>
:host { display: inline-flex; gap: 4px; cursor: pointer; }
.star { font-size: 24px; color: #d1d5db; transition: color 0.15s; }
.star.filled { color: #f59e0b; }
.star:hover { color: #fbbf24; }
</style>
${stars}
`;
}
private setupEvents() {
this.shadowRoot!.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('star')) {
const index = Number(target.dataset.index) + 1;
this.setAttribute('value', String(index));
this.dispatchEvent(new CustomEvent('rate', {
detail: { value: index },
bubbles: true,
composed: true
}));
}
});
}
}
customElements.define('star-rating', StarRating);
Usage:
<star-rating value="3" max="5"></star-rating>
<script>
document.querySelector('star-rating')
.addEventListener('rate', (e) => {
console.log('Rated:', e.detail.value);
});
</script>
Browser Compatibility
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Custom Elements v1 | 54+ | 63+ | 10.1+ | 79+ |
| Shadow DOM v1 | 53+ | 63+ | 10.1+ | 79+ |
| HTML Templates | 35+ | 22+ | 9+ | 79+ |
CSS ::part() |
73+ | 72+ | 13.1+ | 79+ |
adoptedCallback |
73+ | 63+ | 15.4+ | 79+ |
All major browsers have full support as of 2026—no polyfills needed.
Best Practices
- Naming convention: Use
org-componentprefix to avoid collisions (e.g.toolsku-color-picker) - Prefer open mode: Closed mode is hard to debug with limited benefit
- Attribute reflection: Sync key properties to DOM attributes for CSS selectors
- Composed events: Set
composed: trueon events that must cross Shadow DOM - Lazy registration: Use
customElements.whenDefinedto manage dependency order
Summary
Web Components provide framework-agnostic component primitives ideal for building cross-project reusable UI atoms. While DX falls short of React/Vue abstractions, their style encapsulation and native standard advantages are irreplaceable in micro-frontends and design systems.
Use Code Playground to quickly test Web Components code and SVG Editor to create inline component icons.
Try these browser-local tools — no sign-up required →
#Web Components#Shadow DOM#Custom Elements#组件化#浏览器原生