What are Web Components?
Web Components are a set of web platform APIs that allow you to create reusable, encapsulated HTML elements. They consist of three main technologies:
- Custom Elements - Define new HTML elements
- Shadow DOM - Encapsulated DOM and styling
- HTML Templates - Reusable markup patterns
HTML Templates
The <template> element holds HTML that isn't rendered immediately but can be instantiated later with JavaScript.
Basic Template Usage
html
<template id="user-card-template">
    <style>
        .user-card {
            border: 2px solid #3498db;
            border-radius: 8px;
            padding: 16px;
            margin: 8px;
            background: #f8f9fa;
            max-width: 300px;
        }
        
        .avatar {
            width: 60px;
            height: 60px;
            border-radius: 50%;
            object-fit: cover;
            margin-bottom: 12px;
        }
        
        .name {
            font-size: 1.2em;
            font-weight: bold;
            color: #2c3e50;
            margin: 0 0 8px 0;
        }
        
        .email {
            color: #7f8c8d;
            margin: 0;
        }
    </style>
    
    <div class="user-card">
        <img class="avatar" src="" alt="User Avatar">
        <h3 class="name"></h3>
        <p class="email"></p>
    </div>
</template>
<div id="user-container"></div>
<script>
function createUserCard(userData) {
    const template = document.getElementById('user-card-template');
    const clone = template.content.cloneNode(true);
    
    clone.querySelector('.avatar').src = userData.avatar;
    clone.querySelector('.name').textContent = userData.name;
    clone.querySelector('.email').textContent = userData.email;
    
    return clone;
}
// Usage
const users = [
    { name: 'John Doe', email: 'john@example.com', avatar: 'https://via.placeholder.com/60' },
    { name: 'Jane Smith', email: 'jane@example.com', avatar: 'https://via.placeholder.com/60' }
];
const container = document.getElementById('user-container');
users.forEach(user => {
    const userCard = createUserCard(user);
    container.appendChild(userCard);
});
</script>
Custom Elements
Custom Elements allow you to define new HTML tags with custom behavior.
Autonomous Custom Elements
html
<script>
class ToggleButton extends HTMLElement {
    constructor() {
        super();
        this.isToggled = false;
        this.render();
        this.addEventListeners();
    }
    
    // Observed attributes - triggers attributeChangedCallback
    static get observedAttributes() {
        return ['label', 'color'];
    }
    
    render() {
        const label = this.getAttribute('label') || 'Toggle';
        const color = this.getAttribute('color') || '#3498db';
        
        this.innerHTML = `
            <style>
                .toggle-btn {
                    padding: 12px 24px;
                    border: none;
                    border-radius: 6px;
                    font-size: 16px;
                    cursor: pointer;
                    transition: all 0.3s ease;
                    background: ${color};
                    color: white;
                }
                
                .toggle-btn:hover {
                    opacity: 0.8;
                    transform: translateY(-2px);
                }
                
                .toggle-btn.toggled {
                    background: #e74c3c;
                    transform: scale(0.95);
                }
            </style>
            <button class="toggle-btn">${label}</button>
        `;
    }
    
    addEventListeners() {
        const button = this.querySelector('.toggle-btn');
        button.addEventListener('click', () => {
            this.toggle();
        });
    }
    
    toggle() {
        this.isToggled = !this.isToggled;
        const button = this.querySelector('.toggle-btn');
        button.classList.toggle('toggled', this.isToggled);
        
        // Dispatch custom event
        this.dispatchEvent(new CustomEvent('toggle', {
            detail: { toggled: this.isToggled },
            bubbles: true
        }));
    }
    
    // Called when observed attributes change
    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue !== newValue) {
            this.render();
            this.addEventListeners();
        }
    }
}
// Define the custom element
customElements.define('toggle-button', ToggleButton);
</script>
<!-- Usage -->
<toggle-button label="Click Me" color="#2ecc71"></toggle-button>
<toggle-button label="Another Toggle" color="#9b59b6"></toggle-button>
<script>
// Listen for custom events
document.addEventListener('toggle', (e) => {
    console.log('Toggle state:', e.detail.toggled);
});
</script>
Shadow DOM
Shadow DOM provides encapsulation, preventing styles and scripts from leaking in or out.
Basic Shadow DOM Usage
html
<script>
class ShadowCard extends HTMLElement {
    constructor() {
        super();
        
        // Create shadow root
        this.attachShadow({ mode: 'open' });
        
        // Create template
        const template = document.createElement('template');
        template.innerHTML = `
            <style>
                :host {
                    display: block;
                    margin: 16px 0;
                }
                
                .card {
                    background: white;
                    border-radius: 8px;
                    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                    padding: 20px;
                    transition: transform 0.2s ease;
                }
                
                .card:hover {
                    transform: translateY(-4px);
                    box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
                }
                
                .title {
                    font-size: 1.5em;
                    color: #2c3e50;
                    margin: 0 0 12px 0;
                    border-bottom: 2px solid #3498db;
                    padding-bottom: 8px;
                }
                
                ::slotted(p) {
                    color: #555;
                    line-height: 1.6;
                }
                
                ::slotted(.highlight) {
                    background: #fff3cd;
                    padding: 4px 8px;
                    border-radius: 4px;
                }
            </style>
            
            <div class="card">
                <h2 class="title">
                    <slot name="title">Default Title</slot>
                </h2>
                <div class="content">
                    <slot></slot>
                </div>
            </div>
        `;
        
        // Clone template and append to shadow root
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
}
customElements.define('shadow-card', ShadowCard);
</script>
<!-- Usage -->
<shadow-card>
    <span slot="title">My Custom Card</span>
    <p>This is the main content of the card.</p>
    <p class="highlight">This paragraph has special styling!</p>
    <button onclick="alert('Button works!')">Click me</button>
</shadow-card>
Advanced Custom Element with Lifecycle Methods
html
<script>
class CounterComponent extends HTMLElement {
    constructor() {
        super();
        this.count = 0;
        this.attachShadow({ mode: 'open' });
        this.render();
    }
    
    static get observedAttributes() {
        return ['initial-count', 'step', 'max', 'min'];
    }
    
    connectedCallback() {
        console.log('Counter component added to page');
        this.setupEventListeners();
        
        // Initialize count from attribute
        const initialCount = this.getAttribute('initial-count');
        if (initialCount) {
            this.count = parseInt(initialCount, 10);
            this.updateDisplay();
        }
    }
    
    disconnectedCallback() {
        console.log('Counter component removed from page');
        this.cleanup();
    }
    
    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'initial-count' && oldValue !== newValue) {
            this.count = parseInt(newValue, 10) || 0;
            this.updateDisplay();
        }
    }
    
    render() {
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: inline-block;
                    border: 2px solid #3498db;
                    border-radius: 8px;
                    padding: 16px;
                    background: #f8f9fa;
                    user-select: none;
                }
                
                .counter {
                    text-align: center;
                }
                
                .display {
                    font-size: 2em;
                    font-weight: bold;
                    color: #2c3e50;
                    margin: 16px 0;
                    padding: 8px;
                    background: white;
                    border-radius: 4px;
                }
                
                .controls {
                    display: flex;
                    gap: 8px;
                    justify-content: center;
                }
                
                button {
                    padding: 8px 16px;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 14px;
                    transition: background 0.2s ease;
                }
                
                .decrement {
                    background: #e74c3c;
                    color: white;
                }
                
                .increment {
                    background: #2ecc71;
                    color: white;
                }
                
                .reset {
                    background: #95a5a6;
                    color: white;
                }
                
                button:hover {
                    opacity: 0.8;
                }
                
                button:disabled {
                    opacity: 0.5;
                    cursor: not-allowed;
                }
            </style>
            
            <div class="counter">
                <div class="display">${this.count}</div>
                <div class="controls">
                    <button class="decrement">-</button>
                    <button class="reset">Reset</button>
                    <button class="increment">+</button>
                </div>
            </div>
        `;
    }
    
    setupEventListeners() {
        const decrementBtn = this.shadowRoot.querySelector('.decrement');
        const incrementBtn = this.shadowRoot.querySelector('.increment');
        const resetBtn = this.shadowRoot.querySelector('.reset');
        
        decrementBtn.addEventListener('click', () => this.decrement());
        incrementBtn.addEventListener('click', () => this.increment());
        resetBtn.addEventListener('click', () => this.reset());
    }
    
    increment() {
        const step = parseInt(this.getAttribute('step'), 10) || 1;
        const max = parseInt(this.getAttribute('max'), 10);
        
        if (max === undefined || this.count + step <= max) {
            this.count += step;
            this.updateDisplay();
            this.dispatchChangeEvent();
        }
    }
    
    decrement() {
        const step = parseInt(this.getAttribute('step'), 10) || 1;
        const min = parseInt(this.getAttribute('min'), 10);
        
        if (min === undefined || this.count - step >= min) {
            this.count -= step;
            this.updateDisplay();
            this.dispatchChangeEvent();
        }
    }
    
    reset() {
        const initialCount = parseInt(this.getAttribute('initial-count'), 10) || 0;
        this.count = initialCount;
        this.updateDisplay();
        this.dispatchChangeEvent();
    }
    
    updateDisplay() {
        const display = this.shadowRoot.querySelector('.display');
        display.textContent = this.count;
        
        // Update button states
        const min = parseInt(this.getAttribute('min'), 10);
        const max = parseInt(this.getAttribute('max'), 10);
        
        const decrementBtn = this.shadowRoot.querySelector('.decrement');
        const incrementBtn = this.shadowRoot.querySelector('.increment');
        
        decrementBtn.disabled = min !== undefined && this.count <= min;
        incrementBtn.disabled = max !== undefined && this.count >= max;
    }
    
    dispatchChangeEvent() {
        this.dispatchEvent(new CustomEvent('count-changed', {
            detail: { count: this.count },
            bubbles: true
        }));
    }
    
    cleanup() {
        // Clean up any intervals, event listeners, etc.
    }
    
    // Public API
    getValue() {
        return this.count;
    }
    
    setValue(value) {
        this.count = parseInt(value, 10) || 0;
        this.updateDisplay();
    }
}
customElements.define('counter-component', CounterComponent);
</script>
<!-- Usage Examples -->
<counter-component initial-count="5" step="2" min="0" max="20"></counter-component>
<counter-component initial-count="10" step="5"></counter-component>
<script>
document.addEventListener('count-changed', (e) => {
    console.log('Counter value:', e.detail.count);
});
</script>
Customized Built-in Elements
html
<script>
class FancyButton extends HTMLButtonElement {
    constructor() {
        super();
        this.addRippleEffect();
    }
    
    addRippleEffect() {
        this.style.position = 'relative';
        this.style.overflow = 'hidden';
        this.style.padding = '12px 24px';
        this.style.border = 'none';
        this.style.borderRadius = '6px';
        this.style.background = '#3498db';
        this.style.color = 'white';
        this.style.cursor = 'pointer';
        this.style.transition = 'all 0.3s ease';
        
        this.addEventListener('click', (e) => {
            const ripple = document.createElement('span');
            const rect = this.getBoundingClientRect();
            const size = Math.max(rect.width, rect.height);
            const x = e.clientX - rect.left - size / 2;
            const y = e.clientY - rect.top - size / 2;
            
            ripple.style.cssText = `
                position: absolute;
                width: ${size}px;
                height: ${size}px;
                left: ${x}px;
                top: ${y}px;
                background: rgba(255, 255, 255, 0.5);
                border-radius: 50%;
                transform: scale(0);
                animation: ripple 0.6s linear;
                pointer-events: none;
            `;
            
            this.appendChild(ripple);
            
            setTimeout(() => ripple.remove(), 600);
        });
        
        // Add ripple animation
        const style = document.createElement('style');
        style.textContent = `
            @keyframes ripple {
                to {
                    transform: scale(4);
                    opacity: 0;
                }
            }
        `;
        document.head.appendChild(style);
    }
}
// Define customized built-in element
customElements.define('fancy-button', FancyButton, { extends: 'button' });
</script>
<!-- Usage -->
<button is="fancy-button">Click for Ripple Effect</button>
Slot-based Content Distribution
html
<script>
class TabContainer extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.activeTab = 0;
        this.render();
    }
    
    connectedCallback() {
        this.setupTabs();
    }
    
    render() {
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                    border: 1px solid #ddd;
                    border-radius: 8px;
                    overflow: hidden;
                }
                
                .tab-headers {
                    display: flex;
                    background: #f8f9fa;
                    border-bottom: 1px solid #ddd;
                }
                
                .tab-content {
                    padding: 20px;
                    min-height: 200px;
                }
                
                ::slotted(tab-header) {
                    padding: 12px 20px;
                    cursor: pointer;
                    border-right: 1px solid #ddd;
                    transition: background 0.2s ease;
                }
                
                ::slotted(tab-header:hover) {
                    background: #e9ecef;
                }
                
                ::slotted(tab-header.active) {
                    background: #007bff;
                    color: white;
                }
                
                ::slotted(tab-panel) {
                    display: none;
                }
                
                ::slotted(tab-panel.active) {
                    display: block;
                }
            </style>
            
            <div class="tab-headers">
                <slot name="headers"></slot>
            </div>
            <div class="tab-content">
                <slot name="panels"></slot>
            </div>
        `;
    }
    
    setupTabs() {
        const headers = this.querySelectorAll('tab-header');
        const panels = this.querySelectorAll('tab-panel');
        
        headers.forEach((header, index) => {
            header.addEventListener('click', () => {
                this.switchTab(index);
            });
        });
        
        // Activate first tab
        this.switchTab(0);
    }
    
    switchTab(index) {
        const headers = this.querySelectorAll('tab-header');
        const panels = this.querySelectorAll('tab-panel');
        
        // Remove active class from all
        headers.forEach(h => h.classList.remove('active'));
        panels.forEach(p => p.classList.remove('active'));
        
        // Add active class to selected
        if (headers[index] && panels[index]) {
            headers[index].classList.add('active');
            panels[index].classList.add('active');
            this.activeTab = index;
        }
    }
}
class TabHeader extends HTMLElement {
    constructor() {
        super();
        this.slot = 'headers';
    }
}
class TabPanel extends HTMLElement {
    constructor() {
        super();
        this.slot = 'panels';
    }
}
customElements.define('tab-container', TabContainer);
customElements.define('tab-header', TabHeader);
customElements.define('tab-panel', TabPanel);
</script>
<!-- Usage -->
<tab-container>
    <tab-header>Tab 1</tab-header>
    <tab-header>Tab 2</tab-header>
    <tab-header>Tab 3</tab-header>
    
    <tab-panel>
        <h3>Content for Tab 1</h3>
        <p>This is the first tab's content with some text and elements.</p>
        <button>Button in Tab 1</button>
    </tab-panel>
    
    <tab-panel>
        <h3>Content for Tab 2</h3>
        <p>Second tab with different content.</p>
        <ul>
            <li>List item 1</li>
            <li>List item 2</li>
        </ul>
    </tab-panel>
    
    <tab-panel>
        <h3>Content for Tab 3</h3>
        <p>Third tab content here.</p>
        <img src="https://via.placeholder.com/200x100" alt="Sample image">
    </tab-panel>
</tab-container>
Web Components Best Practices
1. Performance Optimization
html
<script>
class LazyComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.rendered = false;
    }
    
    connectedCallback() {
        // Use Intersection Observer for lazy loading
        this.observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting && !this.rendered) {
                    this.render();
                    this.rendered = true;
                    this.observer.disconnect();
                }
            });
        });
        
        this.observer.observe(this);
    }
    
    disconnectedCallback() {
        if (this.observer) {
            this.observer.disconnect();
        }
    }
    
    render() {
        this.shadowRoot.innerHTML = `
            <style>
                .content {
                    padding: 20px;
                    background: #f0f0f0;
                    border-radius: 8px;
                    animation: fadeIn 0.5s ease-in;
                }
                
                @keyframes fadeIn {
                    from { opacity: 0; }
                    to { opacity: 1; }
                }
            </style>
            <div class="content">
                <h3>Lazy Loaded Content</h3>
                <p>This component only renders when it comes into view!</p>
            </div>
        `;
    }
}
customElements.define('lazy-component', LazyComponent);
</script>
2. Error Handling
html
<script>
class RobustComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.errorBoundary();
    }
    
    errorBoundary() {
        try {
            this.render();
        } catch (error) {
            console.error('Component render error:', error);
            this.renderError();
        }
    }
    
    render() {
        // Potentially error-prone code
        const data = JSON.parse(this.getAttribute('data') || '{}');
        
        this.shadowRoot.innerHTML = `
            <style>
                .success { color: green; }
                .error { color: red; padding: 10px; background: #ffe6e6; }
            </style>
            <div class="success">
                <h3>${data.title || 'Default Title'}</h3>
                <p>${data.description || 'Default description'}</p>
            </div>
        `;
    }
    
    renderError() {
        this.shadowRoot.innerHTML = `
            <style>
                .error { 
                    color: red; 
                    padding: 10px; 
                    background: #ffe6e6;
                    border-radius: 4px;
                }
            </style>
            <div class="error">
                ⚠️ Component failed to render. Please check the data attribute.
            </div>
        `;
    }
}
customElements.define('robust-component', RobustComponent);
</script>
<!-- Usage -->
<robust-component data='{"title": "Working Component", "description": "This works fine"}'></robust-component>
<robust-component data='invalid json'></robust-component>
3. Form Integration
html
<script>
class CustomInput extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.render();
    }
    
    static get formAssociated() { return true; }
    
    connectedCallback() {
        this.internals = this.attachInternals();
        this.setupFormIntegration();
    }
    
    render() {
        this.shadowRoot.innerHTML = `
            <style>
                .input-wrapper {
                    display: flex;
                    flex-direction: column;
                    gap: 4px;
                }
                
                label {
                    font-weight: bold;
                    color: #333;
                }
                
                input {
                    padding: 8px 12px;
                    border: 2px solid #ddd;
                    border-radius: 4px;
                    font-size: 14px;
                }
                
                input:focus {
                    outline: none;
                    border-color: #007bff;
                }
                
                .error {
                    color: red;
                    font-size: 12px;
                }
            </style>
            
            <div class="input-wrapper">
                <label for="input">${this.getAttribute('label') || 'Input'}</label>
                <input type="text" id="input" 
                       placeholder="${this.getAttribute('placeholder') || ''}"
                       value="${this.getAttribute('value') || ''}">
                <div class="error"></div>
            </div>
        `;
    }
    
    setupFormIntegration() {
        const input = this.shadowRoot.querySelector('input');
        const errorDiv = this.shadowRoot.querySelector('.error');
        
        input.addEventListener('input', (e) => {
            this.internals.setFormValue(e.target.value);
            this.validate(e.target.value);
        });
        
        input.addEventListener('blur', () => {
            this.validate(input.value);
        });
    }
    
    validate(value) {
        const errorDiv = this.shadowRoot.querySelector('.error');
        const required = this.hasAttribute('required');
        const minLength = this.getAttribute('min-length');
        
        if (required && !value.trim()) {
            this.internals.setValidity({valueMissing: true}, 'This field is required');
            errorDiv.textContent = 'This field is required';
            return false;
        }
        
        if (minLength && value.length < parseInt(minLength)) {
            this.internals.setValidity({tooShort: true}, `Minimum ${minLength} characters required`);
            errorDiv.textContent = `Minimum ${minLength} characters required`;
            return false;
        }
        
        this.internals.setValidity({});
        errorDiv.textContent = '';
        return true;
    }
    
    // Form API methods
    get value() {
        return this.shadowRoot.querySelector('input').value;
    }
    
    set value(val) {
        const input = this.shadowRoot.querySelector('input');
        input.value = val;
        this.internals.setFormValue(val);
    }
    
    get name() {
        return this.getAttribute('name');
    }
    
    get form() {
        return this.internals.form;
    }
    
    get validity() {
        return this.internals.validity;
    }
    
    get validationMessage() {
        return this.internals.validationMessage;
    }
    
    checkValidity() {
        return this.internals.checkValidity();
    }
    
    reportValidity() {
        return this.internals.reportValidity();
    }
}
customElements.define('custom-input', CustomInput);
</script>
<!-- Usage in forms -->
<form id="test-form">
    <custom-input name="username" label="Username" required min-length="3"></custom-input>
    <custom-input name="email" label="Email" placeholder="Enter your email" required></custom-input>
    <button type="submit">Submit</button>
</form>
<script>
document.getElementById('test-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    console.log('Form data:', Object.fromEntries(formData));
});
</script>
Testing Web Components
html
<script>
// Simple testing framework for components
class ComponentTester {
    constructor(componentName) {
        this.componentName = componentName;
        this.tests = [];
    }
    
    test(name, testFn) {
        this.tests.push({ name, testFn });
        return this;
    }
    
    async run() {
        console.log(`Running tests for ${this.componentName}:`);
        
        for (const test of this.tests) {
            try {
                await test.testFn();
                console.log(`✓ ${test.name}`);
            } catch (error) {
                console.error(`✗ ${test.name}:`, error.message);
            }
        }
    }
}
// Example tests for counter component
const counterTests = new ComponentTester('counter-component')
    .test('should render with initial count', () => {
        const counter = document.createElement('counter-component');
        counter.setAttribute('initial-count', '5');
        document.body.appendChild(counter);
        
        const display = counter.shadowRoot.querySelector('.display');
        if (display.textContent !== '5') {
            throw new Error(`Expected '5', got '${display.textContent}'`);
        }
        
        document.body.removeChild(counter);
    })
    .test('should increment count', () => {
        const counter = document.createElement('counter-component');
        document.body.appendChild(counter);
        
        const incrementBtn = counter.shadowRoot.querySelector('.increment');
        incrementBtn.click();
        
        const display = counter.shadowRoot.querySelector('.display');
        if (display.textContent !== '1') {
            throw new Error(`Expected '1', got '${display.textContent}'`);
        }
        
        document.body.removeChild(counter);
    })
    .test('should respect max attribute', () => {
        const counter = document.createElement('counter-component');
        counter.setAttribute('initial-count', '5');
        counter.setAttribute('max', '5');
        document.body.appendChild(counter);
        
        const incrementBtn = counter.shadowRoot.querySelector('.increment');
        incrementBtn.click();
        
        const display = counter.shadowRoot.querySelector('.display');
        if (display.textContent !== '5') {
            throw new Error(`Expected '5' (max reached), got '${display.textContent}'`);
        }
        
        document.body.removeChild(counter);
    });
// Run tests when page loads
document.addEventListener('DOMContentLoaded', () => {
    counterTests.run();
});
</script>