Docs
19.8-Web-Components
19.8 Web Components
Overview
Web Components are a set of web platform APIs that allow you to create reusable, encapsulated custom HTML elements. They work across modern browsers without frameworks.
Learning Objectives
- •Create Custom Elements with ES6 classes
- •Use Shadow DOM for style encapsulation
- •Work with HTML Templates and Slots
- •Understand the Custom Element lifecycle
- •Build reusable component libraries
Custom Elements
Basic Custom Element
class MyElement extends HTMLElement {
constructor() {
super();
this.textContent = 'Hello from Custom Element!';
}
}
// Register the element
customElements.define('my-element', MyElement);
<my-element></my-element>
Lifecycle Callbacks
class LifecycleElement extends HTMLElement {
constructor() {
super();
console.log('Constructor called');
}
// Called when element is added to DOM
connectedCallback() {
console.log('Element added to page');
}
// Called when element is removed from DOM
disconnectedCallback() {
console.log('Element removed from page');
}
// Called when element is moved to new document
adoptedCallback() {
console.log('Element moved to new document');
}
// Called when observed attributes change
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
// Specify which attributes to observe
static get observedAttributes() {
return ['title', 'count'];
}
}
customElements.define('lifecycle-element', LifecycleElement);
Shadow DOM
Encapsulated Styling
class ShadowElement extends HTMLElement {
constructor() {
super();
// Create shadow root
const shadow = this.attachShadow({ mode: 'open' });
// Add content
shadow.innerHTML = `
<style>
/* Styles are scoped to this component */
:host {
display: block;
padding: 16px;
border: 1px solid #ccc;
}
:host([theme="dark"]) {
background: #333;
color: white;
}
h2 {
color: blue;
margin: 0;
}
</style>
<h2>Shadow DOM Content</h2>
<p>This is encapsulated</p>
`;
}
}
customElements.define('shadow-element', ShadowElement);
Shadow DOM Modes
// Open - shadow root accessible via element.shadowRoot
this.attachShadow({ mode: 'open' });
// Closed - shadow root not accessible
const shadow = this.attachShadow({ mode: 'closed' });
HTML Templates
Using Templates
<template id="my-template">
<style>
.container {
padding: 20px;
background: #f0f0f0;
}
</style>
<div class="container">
<h2><slot name="title">Default Title</slot></h2>
<p><slot>Default content</slot></p>
</div>
</template>
class TemplateElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('my-template');
const content = template.content.cloneNode(true);
shadow.appendChild(content);
}
}
customElements.define('template-element', TemplateElement);
Using Slots
<template-element>
<span slot="title">Custom Title</span>
<p>This goes in the default slot</p>
</template-element>
Reactive Properties
class ReactiveElement extends HTMLElement {
static get observedAttributes() {
return ['count'];
}
constructor() {
super();
this._count = 0;
this.attachShadow({ mode: 'open' });
this.render();
}
get count() {
return this._count;
}
set count(value) {
this._count = parseInt(value) || 0;
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'count') {
this.count = newValue;
}
}
connectedCallback() {
this.shadowRoot
.querySelector('button')
.addEventListener('click', () => this.increment());
}
increment() {
this.count++;
this.setAttribute('count', this.count);
this.dispatchEvent(
new CustomEvent('count-changed', {
detail: { count: this.count },
})
);
}
render() {
this.shadowRoot.innerHTML = `
<style>
button { padding: 10px 20px; }
span { margin-left: 10px; font-size: 1.5em; }
</style>
<button>Increment</button>
<span>${this.count}</span>
`;
}
}
customElements.define('reactive-element', ReactiveElement);
Custom Events
class EventElement extends HTMLElement {
connectedCallback() {
this.addEventListener('click', () => {
// Dispatch custom event
this.dispatchEvent(
new CustomEvent('my-event', {
bubbles: true, // Event bubbles up
composed: true, // Crosses shadow DOM boundary
detail: {
// Custom data
message: 'Hello from custom event',
},
})
);
});
}
}
customElements.define('event-element', EventElement);
// Listen for custom event
document.querySelector('event-element').addEventListener('my-event', (e) => {
console.log(e.detail.message);
});
Extending Built-in Elements
class FancyButton extends HTMLButtonElement {
constructor() {
super();
this.style.background = 'linear-gradient(45deg, #ff6b6b, #feca57)';
this.style.border = 'none';
this.style.padding = '10px 20px';
this.style.color = 'white';
this.style.borderRadius = '5px';
this.style.cursor = 'pointer';
}
connectedCallback() {
this.addEventListener('mouseenter', () => {
this.style.transform = 'scale(1.05)';
});
this.addEventListener('mouseleave', () => {
this.style.transform = 'scale(1)';
});
}
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
<button is="fancy-button">Click Me</button>
Form-Associated Custom Elements
class CustomInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this._internals = this.attachInternals();
this._value = '';
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<input type="text" />
`;
shadow.querySelector('input').addEventListener('input', (e) => {
this._value = e.target.value;
this._internals.setFormValue(this._value);
});
}
get value() {
return this._value;
}
set value(v) {
this._value = v;
this.shadowRoot.querySelector('input').value = v;
this._internals.setFormValue(v);
}
// Form callbacks
formResetCallback() {
this.value = '';
}
formStateRestoreCallback(state) {
this.value = state;
}
}
customElements.define('custom-input', CustomInput);
Best Practices
- •Use meaningful names - Custom element names must contain a hyphen
- •Leverage Shadow DOM - For style encapsulation
- •Define observed attributes - Only observe what you need
- •Cleanup in disconnectedCallback - Remove event listeners, observers
- •Use slots for composition - Allow flexible content projection
- •Dispatch meaningful events - For component communication
Browser Support
// Check for Web Components support
const supportsWebComponents =
'customElements' in window &&
'attachShadow' in Element.prototype &&
'content' in document.createElement('template');
if (!supportsWebComponents) {
// Load polyfills
}
Summary
| Feature | Purpose |
|---|---|
| Custom Elements | Define new HTML elements |
| Shadow DOM | Encapsulate styles and markup |
| Templates | Reusable markup fragments |
| Slots | Content projection |
| Lifecycle Callbacks | React to element lifecycle |