HTML Web Components
Introduction to Web Components
Web Components are a set of standardized browser APIs that allow you to create custom, reusable, and encapsulated HTML elements. They are built on existing web standards and work across modern browsers.
- Custom Elements: Define new HTML elements with their own behavior
- Shadow DOM: Encapsulate styles and markup to prevent conflicts
- HTML Templates: Define fragments of markup that can be reused
- ES Modules: The standard for JavaScript modules to package components
Benefits of Web Components
- Reusability: Create components once, use them anywhere
- Encapsulation: Keep markup structure, style, and behavior hidden from the rest of the page
- Standardization: Based on web standards, not framework-specific
- Interoperability: Work with any JavaScript library or framework
- Maintainability: Components can be updated independently
Custom Elements
Custom Elements allow you to define your own HTML elements with custom behavior. There are two types of custom elements:
- Autonomous custom elements: Standalone elements that don't inherit from standard HTML elements
- Customized built-in elements: Elements that extend existing HTML elements
Creating a Custom Element
// Define a new custom element
class UserCard extends HTMLElement {
constructor() {
super(); // Always call super() first
// Element functionality written in here
this.innerHTML = `
${this.getAttribute('name')}
${this.getAttribute('email')}
`;
}
}
// Register the custom element
customElements.define('user-card', UserCard);
Using a Custom Element
<!-- Using the custom element in HTML -->
<user-card name="John Doe" email="john@example.com"></user-card>
Lifecycle Callbacks
Custom elements provide special lifecycle callbacks that allow you to run code at specific points:
class MyElement extends HTMLElement {
// Called when element is inserted into the DOM
connectedCallback() {
console.log('Element added to page');
}
// Called when element is removed from the DOM
disconnectedCallback() {
console.log('Element removed from page');
}
// Called when an observed attribute changes
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
// Specify which attributes to observe
static get observedAttributes() {
return ['title', 'status'];
}
// Called when element is moved to a new document
adoptedCallback() {
console.log('Element moved to new document');
}
}
Shadow DOM
Shadow DOM provides encapsulation for DOM and CSS, keeping the element's internal structure separate from the rest of the page.

Image source: Google Developers. © Google LLC. All rights reserved.
Creating and Using Shadow DOM
class FancyButton extends HTMLElement {
constructor() {
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create element
const button = document.createElement('button');
button.textContent = this.textContent;
// Create styles
const style = document.createElement('style');
style.textContent = `
button {
background: #ff7614;
border: none;
border-radius: 4px;
color: white;
padding: 8px 16px;
font-size: 16px;
}
`;
// Attach elements to shadow DOM
shadow.appendChild(style);
shadow.appendChild(button);
}
}
customElements.define('fancy-button', FancyButton);
Using the Custom Element with Shadow DOM
<fancy-button>Click Me</fancy-button>
Shadow DOM Modes
- Open mode: Outside JavaScript can access the shadow DOM
- Closed mode: Outside JavaScript cannot access the shadow DOM
// Open mode (accessible)
const openShadow = element.attachShadow({mode: 'open'});
// Later, can be accessed:
element.shadowRoot // returns the shadow root
// Closed mode (not accessible)
const closedShadow = element.attachShadow({mode: 'closed'});
// Later:
element.shadowRoot // returns null
Styling in Shadow DOM
Styles defined inside Shadow DOM are scoped to the shadow tree and don't affect elements outside:
// Styles are scoped to the shadow DOM
const style = document.createElement('style');
style.textContent = `
/* Only affects elements in this shadow DOM */
p {
color: red;
}
/* Using CSS custom properties (variables) from outside */
button {
background-color: var(--button-bg, blue);
}
`;
HTML Templates
The <template>
element allows you to define HTML fragments that can be cloned and inserted into the document. Content inside a template is not rendered until it's activated.
Creating a Template
<!-- Define a template -->
<template id="user-template">
<div class="user-card">
<img class="avatar">
<div class="user-info">
<h2 class="name"></h2>
<p class="email"></p>
</div>
</div>
</template>
Using a Template with Custom Elements
class UserProfile extends HTMLElement {
constructor() {
super();
// Create shadow DOM
const shadow = this.attachShadow({mode: 'open'});
// Get the template content
const template = document.getElementById('user-template');
const templateContent = template.content;
// Clone the template
const clone = templateContent.cloneNode(true);
// Customize the content
const img = clone.querySelector('.avatar');
img.src = this.getAttribute('avatar') || 'default-avatar.png';
const name = clone.querySelector('.name');
name.textContent = this.getAttribute('name');
const email = clone.querySelector('.email');
email.textContent = this.getAttribute('email');
// Add styles
const style = document.createElement('style');
style.textContent = `
.user-card {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
}
.name {
margin: 0 0 5px 0;
}
.email {
margin: 0;
color: #666;
}
`;
// Attach the elements to the shadow DOM
shadow.appendChild(style);
shadow.appendChild(clone);
}
}
customElements.define('user-profile', UserProfile);
Using the Custom Element
<user-profile
name="Jane Smith"
email="jane@example.com"
avatar="https://randomuser.me/api/portraits/women/17.jpg">
</user-profile>
Slots
Slots allow you to create placeholders in your component that can be filled with user-provided content, making your components more flexible.
Basic Slot Usage
class CardComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `
Default Header
Default content
`;
}
}
customElements.define('card-component', CardComponent);
Using Slots in HTML
<card-component>
<h2 slot="header">Card Title</h2>
<p>This is the main content of the card.</p>
<small slot="footer">Created on April 24, 2025</small>
</card-component>
Card Title
This is the main content of the card. It demonstrates how slots work in Web Components.
Created on April 24, 2025Slot Events and API
You can detect when slot content changes using the slotchange
event:
connectedCallback() {
const slots = this.shadowRoot.querySelectorAll('slot');
slots.forEach(slot => {
slot.addEventListener('slotchange', (e) => {
console.log('Slot content changed:', slot.name);
console.log('Assigned elements:', slot.assignedElements());
});
});
}
Browser Support and Polyfills
Web Components are supported in all modern browsers, but older browsers may need polyfills.
Current Browser Support
- Chrome: Full support
- Firefox: Full support
- Safari: Full support
- Edge (Chromium-based): Full support
- Edge (Legacy): Partial support with polyfills
- Internet Explorer: No support, requires polyfills
Using Polyfills
For older browsers, you can use the WebComponents.js polyfills:
<!-- Load polyfills only when needed -->
<script>
if (!('customElements' in window)) {
document.write('<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"><\/script>');
}
</script>
Best Practices
Naming Conventions
- Custom element names must contain a hyphen (-) to differentiate them from native elements
- Use descriptive, semantic names (e.g.,
user-profile
, notmy-component
) - Consider a namespace prefix for your organization (e.g.,
acme-button
)
Performance Considerations
- Minimize DOM operations by using document fragments
- Lazy-load components that aren't immediately visible
- Use
:host
and:defined
CSS selectors to prevent FOUC (Flash of Unstyled Content) - Consider using
adoptedStyleSheets
for shared styles across components
Accessibility
- Ensure proper keyboard navigation within components
- Use appropriate ARIA attributes
- Maintain proper focus management
- Test with screen readers and other assistive technologies
// Example of an accessible toggle button
class AccessibleToggle extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
this._button = document.createElement('button');
this._button.textContent = this.textContent || 'Toggle';
this._button.setAttribute('aria-pressed', 'false');
shadow.appendChild(this._button);
this._button.addEventListener('click', () => {
const isPressed = this._button.getAttribute('aria-pressed') === 'true';
this._button.setAttribute('aria-pressed', (!isPressed).toString());
});
}
}
Resources and Further Reading
Documentation
Libraries and Tools
- Lit - Google's lightweight library for building Web Components
- Stencil - Compiler for generating Web Components
- Open Web Components - Recommendations and tools
- WebComponents Polyfills
Component Collections
- Shoelace - A collection of professionally designed Web Components
- FAST - Microsoft's adaptive UI system built on Web Components
- Material Web Components - Google's Material Design implemented as Web Components