JavaScript Components

JavaScript Web Components: Custom Elements, Shadow DOM, and HTML Templates

Master Web Components with JavaScript. Learn to build reusable custom elements, encapsulated styling with Shadow DOM, and dynamic templates.

By JavaScript Document Team
web-componentscustom-elementsshadow-domtemplatesencapsulationreusable-components

Web Components are a set of web platform APIs that allow you to create reusable, encapsulated HTML elements. They consist of four main technologies: Custom Elements, Shadow DOM, HTML Templates, and HTML Imports. This comprehensive guide covers building robust, reusable components using modern JavaScript.

Web Components Fundamentals

Custom Elements API

Custom Elements allow you to define new HTML tags with custom behavior.

// Basic Custom Element
class SimpleButton extends HTMLElement {
  constructor() {
    super();

    // Set up initial state
    this.state = {
      disabled: false,
      variant: 'primary',
      size: 'medium',
    };

    // Bind methods to maintain context
    this.handleClick = this.handleClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
  }

  // Lifecycle callback - element added to DOM
  connectedCallback() {
    this.render();
    this.setupEventListeners();
    this.setupAttributes();
  }

  // Lifecycle callback - element removed from DOM
  disconnectedCallback() {
    this.cleanup();
  }

  // Lifecycle callback - attributes changed
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.handleAttributeChange(name, newValue);
    }
  }

  // Lifecycle callback - element moved to new document
  adoptedCallback() {
    this.render();
  }

  // Define which attributes to observe
  static get observedAttributes() {
    return ['disabled', 'variant', 'size', 'type'];
  }

  // Handle attribute changes
  handleAttributeChange(name, value) {
    switch (name) {
      case 'disabled':
        this.state.disabled = value !== null;
        this.updateDisabledState();
        break;
      case 'variant':
        this.state.variant = value || 'primary';
        this.updateVariant();
        break;
      case 'size':
        this.state.size = value || 'medium';
        this.updateSize();
        break;
      case 'type':
        this.updateType(value);
        break;
    }
  }

  // Setup initial attributes
  setupAttributes() {
    // Set default attributes if not present
    if (!this.hasAttribute('variant')) {
      this.setAttribute('variant', 'primary');
    }
    if (!this.hasAttribute('size')) {
      this.setAttribute('size', 'medium');
    }
    if (!this.hasAttribute('type')) {
      this.setAttribute('type', 'button');
    }
  }

  // Render the component
  render() {
    this.innerHTML = `
      <style>
        :host {
          display: inline-block;
          font-family: system-ui, -apple-system, sans-serif;
        }
        
        button {
          border: none;
          border-radius: 4px;
          font-weight: 500;
          cursor: pointer;
          transition: all 0.2s ease;
          outline: none;
          font-family: inherit;
        }
        
        button:focus {
          box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
        }
        
        button:disabled {
          opacity: 0.5;
          cursor: not-allowed;
        }
        
        /* Variants */
        .primary {
          background: #3b82f6;
          color: white;
        }
        
        .primary:hover:not(:disabled) {
          background: #2563eb;
        }
        
        .secondary {
          background: #6b7280;
          color: white;
        }
        
        .secondary:hover:not(:disabled) {
          background: #4b5563;
        }
        
        .outline {
          background: transparent;
          border: 1px solid #d1d5db;
          color: #374151;
        }
        
        .outline:hover:not(:disabled) {
          background: #f9fafb;
        }
        
        /* Sizes */
        .small {
          padding: 6px 12px;
          font-size: 12px;
        }
        
        .medium {
          padding: 8px 16px;
          font-size: 14px;
        }
        
        .large {
          padding: 12px 24px;
          font-size: 16px;
        }
      </style>
      <button class="${this.state.variant} ${this.state.size}">
        <slot></slot>
      </button>
    `;

    this.button = this.querySelector('button');
  }

  // Set up event listeners
  setupEventListeners() {
    if (this.button) {
      this.button.addEventListener('click', this.handleClick);
      this.button.addEventListener('keydown', this.handleKeyDown);
    }
  }

  // Handle click events
  handleClick(event) {
    if (this.state.disabled) {
      event.preventDefault();
      event.stopPropagation();
      return;
    }

    // Dispatch custom event
    this.dispatchEvent(
      new CustomEvent('simple-button-click', {
        detail: {
          variant: this.state.variant,
          size: this.state.size,
          timestamp: Date.now(),
        },
        bubbles: true,
        composed: true,
      })
    );
  }

  // Handle keyboard events
  handleKeyDown(event) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.handleClick(event);
    }
  }

  // Update disabled state
  updateDisabledState() {
    if (this.button) {
      this.button.disabled = this.state.disabled;
    }
  }

  // Update variant
  updateVariant() {
    if (this.button) {
      this.button.className = `${this.state.variant} ${this.state.size}`;
    }
  }

  // Update size
  updateSize() {
    if (this.button) {
      this.button.className = `${this.state.variant} ${this.state.size}`;
    }
  }

  // Update type
  updateType(type) {
    if (this.button) {
      this.button.type = type || 'button';
    }
  }

  // Cleanup method
  cleanup() {
    if (this.button) {
      this.button.removeEventListener('click', this.handleClick);
      this.button.removeEventListener('keydown', this.handleKeyDown);
    }
  }

  // Public API methods
  focus() {
    if (this.button) {
      this.button.focus();
    }
  }

  blur() {
    if (this.button) {
      this.button.blur();
    }
  }

  disable() {
    this.setAttribute('disabled', '');
  }

  enable() {
    this.removeAttribute('disabled');
  }
}

// Register the custom element
customElements.define('simple-button', SimpleButton);

// Usage examples
document.body.innerHTML += `
  <simple-button variant="primary" size="large">Primary Button</simple-button>
  <simple-button variant="secondary">Secondary Button</simple-button>
  <simple-button variant="outline" size="small">Outline Button</simple-button>
  <simple-button disabled>Disabled Button</simple-button>
`;

// Listen for custom events
document.addEventListener('simple-button-click', (event) => {
  console.log('Button clicked:', event.detail);
});

Shadow DOM

Shadow DOM provides encapsulation for your component's internal structure and styling.

// Advanced Component with Shadow DOM
class AdvancedCard extends HTMLElement {
  constructor() {
    super();

    // Create shadow root for encapsulation
    this.attachShadow({ mode: 'open' });

    // Component state
    this.state = {
      expanded: false,
      loading: false,
      data: null,
    };

    // Bind methods
    this.toggleExpanded = this.toggleExpanded.bind(this);
    this.handleSlotChange = this.handleSlotChange.bind(this);
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
    this.setupSlotObserver();
  }

  disconnectedCallback() {
    this.cleanup();
  }

  static get observedAttributes() {
    return ['title', 'subtitle', 'expanded', 'loading'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.handleAttributeChange(name, newValue);
    }
  }

  handleAttributeChange(name, value) {
    switch (name) {
      case 'title':
        this.updateTitle(value);
        break;
      case 'subtitle':
        this.updateSubtitle(value);
        break;
      case 'expanded':
        this.state.expanded = value !== null;
        this.updateExpandedState();
        break;
      case 'loading':
        this.state.loading = value !== null;
        this.updateLoadingState();
        break;
    }
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: system-ui, -apple-system, sans-serif;
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          overflow: hidden;
          transition: box-shadow 0.2s ease;
        }
        
        :host(:hover) {
          box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
        }
        
        :host([loading]) {
          opacity: 0.7;
          pointer-events: none;
        }
        
        .header {
          padding: 16px;
          background: #f9fafb;
          border-bottom: 1px solid #e5e7eb;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        
        .header:hover {
          background: #f3f4f6;
        }
        
        .title-section {
          flex: 1;
        }
        
        .title {
          margin: 0;
          font-size: 18px;
          font-weight: 600;
          color: #111827;
        }
        
        .subtitle {
          margin: 4px 0 0 0;
          font-size: 14px;
          color: #6b7280;
        }
        
        .expand-icon {
          width: 20px;
          height: 20px;
          transition: transform 0.2s ease;
          color: #6b7280;
        }
        
        .expand-icon.expanded {
          transform: rotate(180deg);
        }
        
        .content {
          max-height: 0;
          overflow: hidden;
          transition: max-height 0.3s ease;
        }
        
        .content.expanded {
          max-height: 500px;
        }
        
        .content-inner {
          padding: 16px;
        }
        
        .loading-spinner {
          display: none;
          width: 20px;
          height: 20px;
          border: 2px solid #e5e7eb;
          border-top: 2px solid #3b82f6;
          border-radius: 50%;
          animation: spin 1s linear infinite;
        }
        
        :host([loading]) .loading-spinner {
          display: block;
        }
        
        :host([loading]) .expand-icon {
          display: none;
        }
        
        @keyframes spin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }
        
        /* Slot styling */
        ::slotted([slot="actions"]) {
          margin-top: 12px;
          display: flex;
          gap: 8px;
        }
        
        ::slotted([slot="footer"]) {
          border-top: 1px solid #e5e7eb;
          padding: 12px 16px;
          background: #f9fafb;
          font-size: 12px;
          color: #6b7280;
        }
      </style>
      
      <div class="header" part="header">
        <div class="title-section">
          <h3 class="title" part="title">${this.getAttribute('title') || ''}</h3>
          <p class="subtitle" part="subtitle">${this.getAttribute('subtitle') || ''}</p>
        </div>
        <div class="loading-spinner"></div>
        <svg class="expand-icon ${this.state.expanded ? 'expanded' : ''}" 
             fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                d="M19 9l-7 7-7-7"></path>
        </svg>
      </div>
      
      <div class="content ${this.state.expanded ? 'expanded' : ''}" part="content">
        <div class="content-inner">
          <slot></slot>
          <slot name="actions"></slot>
        </div>
      </div>
      
      <slot name="footer"></slot>
    `;
  }

  setupEventListeners() {
    const header = this.shadowRoot.querySelector('.header');
    if (header) {
      header.addEventListener('click', this.toggleExpanded);
    }
  }

  setupSlotObserver() {
    // Monitor slot changes
    const slots = this.shadowRoot.querySelectorAll('slot');
    slots.forEach((slot) => {
      slot.addEventListener('slotchange', this.handleSlotChange);
    });
  }

  handleSlotChange(event) {
    const slot = event.target;
    const assignedNodes = slot.assignedNodes();

    // Dispatch event when slot content changes
    this.dispatchEvent(
      new CustomEvent('slot-change', {
        detail: {
          slot: slot.name || 'default',
          nodeCount: assignedNodes.length,
        },
        bubbles: true,
      })
    );
  }

  toggleExpanded() {
    if (this.state.loading) return;

    this.state.expanded = !this.state.expanded;

    if (this.state.expanded) {
      this.setAttribute('expanded', '');
    } else {
      this.removeAttribute('expanded');
    }

    // Dispatch custom event
    this.dispatchEvent(
      new CustomEvent('card-toggle', {
        detail: {
          expanded: this.state.expanded,
          title: this.getAttribute('title'),
        },
        bubbles: true,
      })
    );
  }

  updateTitle(title) {
    const titleElement = this.shadowRoot.querySelector('.title');
    if (titleElement) {
      titleElement.textContent = title || '';
    }
  }

  updateSubtitle(subtitle) {
    const subtitleElement = this.shadowRoot.querySelector('.subtitle');
    if (subtitleElement) {
      subtitleElement.textContent = subtitle || '';
      subtitleElement.style.display = subtitle ? 'block' : 'none';
    }
  }

  updateExpandedState() {
    const content = this.shadowRoot.querySelector('.content');
    const expandIcon = this.shadowRoot.querySelector('.expand-icon');

    if (content) {
      content.classList.toggle('expanded', this.state.expanded);
    }

    if (expandIcon) {
      expandIcon.classList.toggle('expanded', this.state.expanded);
    }
  }

  updateLoadingState() {
    // Loading state is handled via CSS and :host([loading]) selector
  }

  cleanup() {
    const header = this.shadowRoot.querySelector('.header');
    if (header) {
      header.removeEventListener('click', this.toggleExpanded);
    }

    const slots = this.shadowRoot.querySelectorAll('slot');
    slots.forEach((slot) => {
      slot.removeEventListener('slotchange', this.handleSlotChange);
    });
  }

  // Public API
  expand() {
    if (!this.state.expanded) {
      this.toggleExpanded();
    }
  }

  collapse() {
    if (this.state.expanded) {
      this.toggleExpanded();
    }
  }

  setLoading(loading) {
    if (loading) {
      this.setAttribute('loading', '');
    } else {
      this.removeAttribute('loading');
    }
  }

  updateContent(content) {
    const defaultSlot = this.querySelector(':not([slot])');
    if (defaultSlot) {
      defaultSlot.innerHTML = content;
    }
  }
}

// Register the advanced card component
customElements.define('advanced-card', AdvancedCard);

// Usage example
document.body.innerHTML += `
  <advanced-card title="Advanced Card" subtitle="With Shadow DOM">
    <p>This content is projected into the card via slots.</p>
    <p>The styling is completely encapsulated.</p>
    
    <div slot="actions">
      <button>Action 1</button>
      <button>Action 2</button>
    </div>
    
    <div slot="footer">
      Last updated: ${new Date().toLocaleDateString()}
    </div>
  </advanced-card>
`;

HTML Templates

HTML Templates provide a way to define reusable markup structures.

// Template-based Component System
class TemplateManager {
  static templates = new Map();

  // Register a template
  static registerTemplate(name, templateString) {
    const template = document.createElement('template');
    template.innerHTML = templateString;
    this.templates.set(name, template);
  }

  // Get a template
  static getTemplate(name) {
    return this.templates.get(name);
  }

  // Clone template content
  static cloneTemplate(name) {
    const template = this.getTemplate(name);
    return template ? template.content.cloneNode(true) : null;
  }

  // Initialize with common templates
  static init() {
    // Card template
    this.registerTemplate(
      'card',
      `
      <style>
        .card {
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          padding: 16px;
          margin: 8px 0;
          background: white;
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }
        .card-header {
          margin-bottom: 12px;
          padding-bottom: 8px;
          border-bottom: 1px solid #e5e7eb;
        }
        .card-title {
          margin: 0;
          font-size: 18px;
          font-weight: 600;
        }
        .card-body {
          margin: 12px 0;
        }
        .card-footer {
          margin-top: 12px;
          padding-top: 8px;
          border-top: 1px solid #e5e7eb;
          font-size: 12px;
          color: #6b7280;
        }
      </style>
      <div class="card" part="card">
        <header class="card-header" part="header">
          <h3 class="card-title" part="title"></h3>
        </header>
        <div class="card-body" part="body">
          <slot></slot>
        </div>
        <footer class="card-footer" part="footer">
          <slot name="footer"></slot>
        </footer>
      </div>
    `
    );

    // Form field template
    this.registerTemplate(
      'form-field',
      `
      <style>
        .form-field {
          margin-bottom: 16px;
        }
        .form-label {
          display: block;
          margin-bottom: 4px;
          font-weight: 500;
          font-size: 14px;
          color: #374151;
        }
        .form-input {
          width: 100%;
          padding: 8px 12px;
          border: 1px solid #d1d5db;
          border-radius: 4px;
          font-size: 14px;
          transition: border-color 0.2s ease;
        }
        .form-input:focus {
          outline: none;
          border-color: #3b82f6;
          box-shadow: 0 0 0 1px #3b82f6;
        }
        .form-error {
          margin-top: 4px;
          font-size: 12px;
          color: #ef4444;
        }
        .form-help {
          margin-top: 4px;
          font-size: 12px;
          color: #6b7280;
        }
      </style>
      <div class="form-field" part="field">
        <label class="form-label" part="label"></label>
        <input class="form-input" part="input">
        <div class="form-error" part="error" style="display: none;"></div>
        <div class="form-help" part="help"></div>
      </div>
    `
    );

    // Modal template
    this.registerTemplate(
      'modal',
      `
      <style>
        .modal-overlay {
          position: fixed;
          top: 0;
          left: 0;
          right: 0;
          bottom: 0;
          background: rgba(0, 0, 0, 0.5);
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 1000;
          opacity: 0;
          visibility: hidden;
          transition: all 0.3s ease;
        }
        .modal-overlay.open {
          opacity: 1;
          visibility: visible;
        }
        .modal-content {
          background: white;
          border-radius: 8px;
          max-width: 500px;
          width: 90%;
          max-height: 80vh;
          overflow-y: auto;
          transform: scale(0.9);
          transition: transform 0.3s ease;
        }
        .modal-overlay.open .modal-content {
          transform: scale(1);
        }
        .modal-header {
          padding: 20px;
          border-bottom: 1px solid #e5e7eb;
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        .modal-title {
          margin: 0;
          font-size: 18px;
          font-weight: 600;
        }
        .modal-close {
          background: none;
          border: none;
          font-size: 24px;
          cursor: pointer;
          color: #6b7280;
        }
        .modal-body {
          padding: 20px;
        }
        .modal-footer {
          padding: 20px;
          border-top: 1px solid #e5e7eb;
          display: flex;
          gap: 8px;
          justify-content: flex-end;
        }
      </style>
      <div class="modal-overlay" part="overlay">
        <div class="modal-content" part="content">
          <header class="modal-header" part="header">
            <h2 class="modal-title" part="title"></h2>
            <button class="modal-close" part="close">&times;</button>
          </header>
          <div class="modal-body" part="body">
            <slot></slot>
          </div>
          <footer class="modal-footer" part="footer">
            <slot name="footer"></slot>
          </footer>
        </div>
      </div>
    `
    );
  }
}

// Initialize templates
TemplateManager.init();

// Template-based Card Component
class TemplateCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.setupAttributes();
  }

  static get observedAttributes() {
    return ['title'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'title' && oldValue !== newValue) {
      this.updateTitle(newValue);
    }
  }

  render() {
    const template = TemplateManager.cloneTemplate('card');
    if (template) {
      this.shadowRoot.appendChild(template);
    }
  }

  setupAttributes() {
    const title = this.getAttribute('title');
    if (title) {
      this.updateTitle(title);
    }
  }

  updateTitle(title) {
    const titleElement = this.shadowRoot.querySelector('.card-title');
    if (titleElement) {
      titleElement.textContent = title || '';
    }
  }
}

// Template-based Form Field Component
class TemplateFormField extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.state = {
      value: '',
      error: '',
      valid: true,
    };
  }

  connectedCallback() {
    this.render();
    this.setupAttributes();
    this.setupEventListeners();
  }

  static get observedAttributes() {
    return ['label', 'type', 'placeholder', 'required', 'help-text'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.handleAttributeChange(name, newValue);
    }
  }

  render() {
    const template = TemplateManager.cloneTemplate('form-field');
    if (template) {
      this.shadowRoot.appendChild(template);
      this.input = this.shadowRoot.querySelector('.form-input');
      this.label = this.shadowRoot.querySelector('.form-label');
      this.error = this.shadowRoot.querySelector('.form-error');
      this.help = this.shadowRoot.querySelector('.form-help');
    }
  }

  setupAttributes() {
    const label = this.getAttribute('label');
    const type = this.getAttribute('type') || 'text';
    const placeholder = this.getAttribute('placeholder');
    const helpText = this.getAttribute('help-text');

    if (this.label && label) {
      this.label.textContent = label;
    }

    if (this.input) {
      this.input.type = type;
      if (placeholder) {
        this.input.placeholder = placeholder;
      }
    }

    if (this.help && helpText) {
      this.help.textContent = helpText;
    }
  }

  setupEventListeners() {
    if (this.input) {
      this.input.addEventListener('input', (e) => {
        this.state.value = e.target.value;
        this.validate();
        this.dispatchEvent(
          new CustomEvent('input', {
            detail: { value: this.state.value },
            bubbles: true,
          })
        );
      });

      this.input.addEventListener('blur', () => {
        this.validate();
      });
    }
  }

  handleAttributeChange(name, value) {
    switch (name) {
      case 'label':
        if (this.label) {
          this.label.textContent = value || '';
        }
        break;
      case 'type':
        if (this.input) {
          this.input.type = value || 'text';
        }
        break;
      case 'placeholder':
        if (this.input) {
          this.input.placeholder = value || '';
        }
        break;
      case 'help-text':
        if (this.help) {
          this.help.textContent = value || '';
        }
        break;
    }
  }

  validate() {
    const required = this.hasAttribute('required');
    const value = this.state.value;

    if (required && !value.trim()) {
      this.setError('This field is required');
      return false;
    }

    // Email validation
    if (this.input.type === 'email' && value) {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(value)) {
        this.setError('Please enter a valid email address');
        return false;
      }
    }

    this.clearError();
    return true;
  }

  setError(message) {
    this.state.error = message;
    this.state.valid = false;

    if (this.error) {
      this.error.textContent = message;
      this.error.style.display = 'block';
    }

    if (this.input) {
      this.input.style.borderColor = '#ef4444';
    }
  }

  clearError() {
    this.state.error = '';
    this.state.valid = true;

    if (this.error) {
      this.error.style.display = 'none';
    }

    if (this.input) {
      this.input.style.borderColor = '#d1d5db';
    }
  }

  // Public API
  get value() {
    return this.state.value;
  }

  set value(val) {
    this.state.value = val;
    if (this.input) {
      this.input.value = val;
    }
  }

  get valid() {
    return this.state.valid;
  }

  focus() {
    if (this.input) {
      this.input.focus();
    }
  }
}

// Template-based Modal Component
class TemplateModal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.state = {
      open: false,
    };

    this.handleOverlayClick = this.handleOverlayClick.bind(this);
    this.handleCloseClick = this.handleCloseClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
  }

  connectedCallback() {
    this.render();
    this.setupAttributes();
    this.setupEventListeners();
  }

  disconnectedCallback() {
    document.removeEventListener('keydown', this.handleKeyDown);
  }

  static get observedAttributes() {
    return ['title', 'open'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.handleAttributeChange(name, newValue);
    }
  }

  render() {
    const template = TemplateManager.cloneTemplate('modal');
    if (template) {
      this.shadowRoot.appendChild(template);
      this.overlay = this.shadowRoot.querySelector('.modal-overlay');
      this.title = this.shadowRoot.querySelector('.modal-title');
      this.closeButton = this.shadowRoot.querySelector('.modal-close');
    }
  }

  setupAttributes() {
    const title = this.getAttribute('title');
    const open = this.hasAttribute('open');

    if (this.title && title) {
      this.title.textContent = title;
    }

    if (open) {
      this.open();
    }
  }

  setupEventListeners() {
    if (this.overlay) {
      this.overlay.addEventListener('click', this.handleOverlayClick);
    }

    if (this.closeButton) {
      this.closeButton.addEventListener('click', this.handleCloseClick);
    }

    document.addEventListener('keydown', this.handleKeyDown);
  }

  handleAttributeChange(name, value) {
    switch (name) {
      case 'title':
        if (this.title) {
          this.title.textContent = value || '';
        }
        break;
      case 'open':
        if (value !== null) {
          this.open();
        } else {
          this.close();
        }
        break;
    }
  }

  handleOverlayClick(event) {
    if (event.target === this.overlay) {
      this.close();
    }
  }

  handleCloseClick() {
    this.close();
  }

  handleKeyDown(event) {
    if (event.key === 'Escape' && this.state.open) {
      this.close();
    }
  }

  open() {
    this.state.open = true;
    if (this.overlay) {
      this.overlay.classList.add('open');
    }

    this.dispatchEvent(
      new CustomEvent('modal-open', {
        bubbles: true,
      })
    );
  }

  close() {
    this.state.open = false;
    if (this.overlay) {
      this.overlay.classList.remove('open');
    }

    this.removeAttribute('open');

    this.dispatchEvent(
      new CustomEvent('modal-close', {
        bubbles: true,
      })
    );
  }
}

// Register template-based components
customElements.define('template-card', TemplateCard);
customElements.define('template-form-field', TemplateFormField);
customElements.define('template-modal', TemplateModal);

// Usage examples
document.body.innerHTML += `
  <template-card title="Template-based Card">
    <p>This card uses a reusable HTML template.</p>
    <div slot="footer">Template system example</div>
  </template-card>
  
  <template-form-field 
    label="Email Address" 
    type="email" 
    placeholder="Enter your email"
    help-text="We'll never share your email"
    required>
  </template-form-field>
  
  <button onclick="document.querySelector('template-modal').setAttribute('open', '')">
    Open Modal
  </button>
  
  <template-modal title="Example Modal">
    <p>This modal is built using HTML templates.</p>
    <div slot="footer">
      <button onclick="document.querySelector('template-modal').close()">Cancel</button>
      <button onclick="document.querySelector('template-modal').close()">Confirm</button>
    </div>
  </template-modal>
`;

Component Communication

Event System

// Event-driven Component Communication
class EventBus {
  constructor() {
    this.events = new Map();
  }

  // Subscribe to an event
  on(event, callback, options = {}) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }

    const subscription = {
      callback,
      once: options.once || false,
      id: Date.now() + Math.random(),
    };

    this.events.get(event).push(subscription);

    // Return unsubscribe function
    return () => this.off(event, subscription.id);
  }

  // Subscribe to an event once
  once(event, callback) {
    return this.on(event, callback, { once: true });
  }

  // Unsubscribe from an event
  off(event, subscriptionId) {
    if (!this.events.has(event)) return;

    const subscriptions = this.events.get(event);
    const index = subscriptions.findIndex((sub) => sub.id === subscriptionId);

    if (index > -1) {
      subscriptions.splice(index, 1);
    }

    if (subscriptions.length === 0) {
      this.events.delete(event);
    }
  }

  // Emit an event
  emit(event, data = null) {
    if (!this.events.has(event)) return;

    const subscriptions = [...this.events.get(event)];

    subscriptions.forEach((subscription) => {
      try {
        subscription.callback(data);

        if (subscription.once) {
          this.off(event, subscription.id);
        }
      } catch (error) {
        console.error(`Error in event handler for ${event}:`, error);
      }
    });
  }

  // Clear all subscriptions for an event
  clear(event) {
    if (event) {
      this.events.delete(event);
    } else {
      this.events.clear();
    }
  }
}

// Global event bus instance
const globalEventBus = new EventBus();

// Parent-Child Communication Component
class ParentComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.state = {
      items: [],
      selectedItem: null,
    };
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
    this.loadInitialData();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
          font-family: system-ui, sans-serif;
        }
        
        .header {
          margin-bottom: 20px;
          padding-bottom: 10px;
          border-bottom: 2px solid #e5e7eb;
        }
        
        .content {
          display: grid;
          grid-template-columns: 1fr 1fr;
          gap: 20px;
        }
        
        .selected-info {
          background: #f3f4f6;
          padding: 16px;
          border-radius: 8px;
        }
      </style>
      
      <div class="header">
        <h2>Parent Component</h2>
        <p>Selected: <span id="selected-display">None</span></p>
      </div>
      
      <div class="content">
        <div>
          <h3>Item List</h3>
          <div id="item-list"></div>
        </div>
        
        <div class="selected-info">
          <h3>Selected Item Details</h3>
          <div id="item-details">No item selected</div>
        </div>
      </div>
    `;
  }

  setupEventListeners() {
    // Listen for child component events
    this.addEventListener('item-selected', (event) => {
      this.handleItemSelected(event.detail);
    });

    this.addEventListener('item-deleted', (event) => {
      this.handleItemDeleted(event.detail);
    });

    // Listen for global events
    globalEventBus.on('refresh-data', () => {
      this.loadInitialData();
    });
  }

  loadInitialData() {
    this.state.items = [
      { id: 1, name: 'Item 1', description: 'First item' },
      { id: 2, name: 'Item 2', description: 'Second item' },
      { id: 3, name: 'Item 3', description: 'Third item' },
    ];

    this.renderItemList();
  }

  renderItemList() {
    const container = this.shadowRoot.getElementById('item-list');
    if (!container) return;

    container.innerHTML = '';

    this.state.items.forEach((item) => {
      const itemElement = document.createElement('child-item');
      itemElement.setAttribute('item-id', item.id);
      itemElement.setAttribute('item-name', item.name);
      itemElement.setAttribute('item-description', item.description);
      container.appendChild(itemElement);
    });
  }

  handleItemSelected(itemData) {
    this.state.selectedItem = itemData;

    const selectedDisplay = this.shadowRoot.getElementById('selected-display');
    const itemDetails = this.shadowRoot.getElementById('item-details');

    if (selectedDisplay) {
      selectedDisplay.textContent = itemData.name;
    }

    if (itemDetails) {
      itemDetails.innerHTML = `
        <p><strong>ID:</strong> ${itemData.id}</p>
        <p><strong>Name:</strong> ${itemData.name}</p>
        <p><strong>Description:</strong> ${itemData.description}</p>
      `;
    }

    // Emit global event
    globalEventBus.emit('item-selection-changed', itemData);
  }

  handleItemDeleted(itemData) {
    this.state.items = this.state.items.filter(
      (item) => item.id !== itemData.id
    );

    if (this.state.selectedItem && this.state.selectedItem.id === itemData.id) {
      this.state.selectedItem = null;
      this.handleItemSelected({ id: null, name: 'None', description: '' });
    }

    this.renderItemList();

    // Emit global event
    globalEventBus.emit('item-deleted', itemData);
  }
}

// Child Component
class ChildItem extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.state = {
      selected: false,
    };

    this.handleClick = this.handleClick.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }

  static get observedAttributes() {
    return ['item-id', 'item-name', 'item-description'];
  }

  render() {
    const id = this.getAttribute('item-id');
    const name = this.getAttribute('item-name');
    const description = this.getAttribute('item-description');

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          margin-bottom: 8px;
        }
        
        .item {
          padding: 12px;
          border: 1px solid #d1d5db;
          border-radius: 6px;
          cursor: pointer;
          transition: all 0.2s ease;
        }
        
        .item:hover {
          background: #f9fafb;
          border-color: #3b82f6;
        }
        
        .item.selected {
          background: #dbeafe;
          border-color: #3b82f6;
        }
        
        .item-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
        }
        
        .item-name {
          font-weight: 600;
          margin: 0;
        }
        
        .item-description {
          margin: 4px 0 0 0;
          color: #6b7280;
          font-size: 14px;
        }
        
        .delete-btn {
          background: #ef4444;
          color: white;
          border: none;
          padding: 4px 8px;
          border-radius: 4px;
          cursor: pointer;
          font-size: 12px;
        }
        
        .delete-btn:hover {
          background: #dc2626;
        }
      </style>
      
      <div class="item ${this.state.selected ? 'selected' : ''}">
        <div class="item-header">
          <h4 class="item-name">${name}</h4>
          <button class="delete-btn">Delete</button>
        </div>
        <p class="item-description">${description}</p>
      </div>
    `;
  }

  setupEventListeners() {
    const item = this.shadowRoot.querySelector('.item');
    const deleteBtn = this.shadowRoot.querySelector('.delete-btn');

    if (item) {
      item.addEventListener('click', this.handleClick);
    }

    if (deleteBtn) {
      deleteBtn.addEventListener('click', this.handleDelete);
    }

    // Listen for global selection changes
    globalEventBus.on('item-selection-changed', (itemData) => {
      const myId = parseInt(this.getAttribute('item-id'));
      this.state.selected = itemData.id === myId;
      this.updateSelectedState();
    });
  }

  handleClick(event) {
    event.stopPropagation();

    const itemData = {
      id: parseInt(this.getAttribute('item-id')),
      name: this.getAttribute('item-name'),
      description: this.getAttribute('item-description'),
    };

    // Dispatch event to parent
    this.dispatchEvent(
      new CustomEvent('item-selected', {
        detail: itemData,
        bubbles: true,
        composed: true,
      })
    );
  }

  handleDelete(event) {
    event.stopPropagation();

    const itemData = {
      id: parseInt(this.getAttribute('item-id')),
      name: this.getAttribute('item-name'),
      description: this.getAttribute('item-description'),
    };

    // Dispatch event to parent
    this.dispatchEvent(
      new CustomEvent('item-deleted', {
        detail: itemData,
        bubbles: true,
        composed: true,
      })
    );
  }

  updateSelectedState() {
    const item = this.shadowRoot.querySelector('.item');
    if (item) {
      item.classList.toggle('selected', this.state.selected);
    }
  }
}

// Register components
customElements.define('parent-component', ParentComponent);
customElements.define('child-item', ChildItem);

// Observer Component - listens to global events
class EventObserver extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.eventLog = [];
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
          background: #f9fafb;
          border-radius: 8px;
          margin-top: 20px;
        }
        
        .log-entry {
          padding: 8px;
          margin: 4px 0;
          background: white;
          border-left: 3px solid #3b82f6;
          border-radius: 4px;
          font-size: 12px;
        }
        
        .timestamp {
          color: #6b7280;
        }
      </style>
      
      <h3>Event Observer</h3>
      <p>Global events will appear here:</p>
      <div id="event-log"></div>
      <button id="clear-log">Clear Log</button>
    `;

    const clearBtn = this.shadowRoot.getElementById('clear-log');
    if (clearBtn) {
      clearBtn.addEventListener('click', () => this.clearLog());
    }
  }

  setupEventListeners() {
    // Listen to various global events
    globalEventBus.on('item-selection-changed', (data) => {
      this.logEvent('Item Selected', data);
    });

    globalEventBus.on('item-deleted', (data) => {
      this.logEvent('Item Deleted', data);
    });

    globalEventBus.on('refresh-data', () => {
      this.logEvent('Data Refreshed', null);
    });
  }

  logEvent(eventType, data) {
    const timestamp = new Date().toLocaleTimeString();
    const entry = {
      type: eventType,
      data,
      timestamp,
    };

    this.eventLog.unshift(entry);

    // Keep only last 10 entries
    if (this.eventLog.length > 10) {
      this.eventLog = this.eventLog.slice(0, 10);
    }

    this.renderLog();
  }

  renderLog() {
    const logContainer = this.shadowRoot.getElementById('event-log');
    if (!logContainer) return;

    logContainer.innerHTML = this.eventLog
      .map(
        (entry) => `
      <div class="log-entry">
        <strong>${entry.type}</strong>
        <span class="timestamp">(${entry.timestamp})</span>
        ${entry.data ? `<br>Data: ${JSON.stringify(entry.data)}` : ''}
      </div>
    `
      )
      .join('');
  }

  clearLog() {
    this.eventLog = [];
    this.renderLog();
  }
}

customElements.define('event-observer', EventObserver);

// Usage example
document.body.innerHTML += `
  <parent-component></parent-component>
  <event-observer></event-observer>
  
  <button onclick="globalEventBus.emit('refresh-data')">
    Refresh Data (Global Event)
  </button>
`;

Component Lifecycle and State Management

// Advanced Component with Complete Lifecycle Management
class AdvancedLifecycleComponent extends HTMLElement {
  constructor() {
    super();

    // Initialize shadow DOM
    this.attachShadow({ mode: 'open' });

    // Component state
    this.state = {
      initialized: false,
      connected: false,
      data: null,
      loading: false,
      error: null,
      subscriptions: [],
    };

    // Lifecycle hooks
    this.onInit = this.onInit.bind(this);
    this.onMount = this.onMount.bind(this);
    this.onUpdate = this.onUpdate.bind(this);
    this.onUnmount = this.onUnmount.bind(this);

    // Initialize component
    this.onInit();
  }

  // Custom lifecycle hooks
  onInit() {
    console.log('Component initialized');
    this.state.initialized = true;
  }

  onMount() {
    console.log('Component mounted');
    this.loadData();
  }

  onUpdate(changes) {
    console.log('Component updated', changes);
  }

  onUnmount() {
    console.log('Component unmounted');
    this.cleanup();
  }

  // Built-in lifecycle callbacks
  connectedCallback() {
    this.state.connected = true;
    this.render();
    this.setupEventListeners();
    this.onMount();
  }

  disconnectedCallback() {
    this.state.connected = false;
    this.onUnmount();
  }

  static get observedAttributes() {
    return ['data-source', 'auto-refresh', 'refresh-interval'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      const changes = { [name]: { oldValue, newValue } };
      this.handleAttributeChange(name, newValue);
      this.onUpdate(changes);
    }
  }

  adoptedCallback() {
    console.log('Component adopted to new document');
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 20px;
          border: 1px solid #e5e7eb;
          border-radius: 8px;
          font-family: system-ui, sans-serif;
        }
        
        .loading {
          display: flex;
          align-items: center;
          gap: 8px;
          color: #6b7280;
        }
        
        .spinner {
          width: 16px;
          height: 16px;
          border: 2px solid #e5e7eb;
          border-top: 2px solid #3b82f6;
          border-radius: 50%;
          animation: spin 1s linear infinite;
        }
        
        .error {
          padding: 12px;
          background: #fef2f2;
          border: 1px solid #fecaca;
          border-radius: 4px;
          color: #dc2626;
        }
        
        .data-display {
          background: #f9fafb;
          padding: 16px;
          border-radius: 4px;
          margin: 12px 0;
        }
        
        .status {
          font-size: 12px;
          color: #6b7280;
          margin-top: 12px;
        }
        
        @keyframes spin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }
      </style>
      
      <h3>Advanced Lifecycle Component</h3>
      
      <div id="content">
        ${this.renderContent()}
      </div>
      
      <div class="status">
        <p>Initialized: ${this.state.initialized}</p>
        <p>Connected: ${this.state.connected}</p>
        <p>Data Source: ${this.getAttribute('data-source') || 'None'}</p>
        <p>Auto Refresh: ${this.hasAttribute('auto-refresh')}</p>
      </div>
      
      <button id="refresh-btn">Refresh Data</button>
      <button id="clear-btn">Clear Data</button>
    `;

    this.setupButtons();
  }

  renderContent() {
    if (this.state.loading) {
      return `
        <div class="loading">
          <div class="spinner"></div>
          <span>Loading data...</span>
        </div>
      `;
    }

    if (this.state.error) {
      return `
        <div class="error">
          <strong>Error:</strong> ${this.state.error}
        </div>
      `;
    }

    if (this.state.data) {
      return `
        <div class="data-display">
          <h4>Data Loaded:</h4>
          <pre>${JSON.stringify(this.state.data, null, 2)}</pre>
        </div>
      `;
    }

    return '<p>No data loaded</p>';
  }

  setupButtons() {
    const refreshBtn = this.shadowRoot.getElementById('refresh-btn');
    const clearBtn = this.shadowRoot.getElementById('clear-btn');

    if (refreshBtn) {
      refreshBtn.addEventListener('click', () => this.loadData());
    }

    if (clearBtn) {
      clearBtn.addEventListener('click', () => this.clearData());
    }
  }

  setupEventListeners() {
    // Set up auto-refresh if enabled
    if (this.hasAttribute('auto-refresh')) {
      this.setupAutoRefresh();
    }
  }

  setupAutoRefresh() {
    const interval = parseInt(this.getAttribute('refresh-interval')) || 5000;

    const intervalId = setInterval(() => {
      if (this.state.connected) {
        this.loadData();
      }
    }, interval);

    // Store subscription for cleanup
    this.state.subscriptions.push(() => {
      clearInterval(intervalId);
    });
  }

  async loadData() {
    this.setState({ loading: true, error: null });

    try {
      const dataSource = this.getAttribute('data-source');

      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve, 1000));

      const data = {
        source: dataSource || 'default',
        timestamp: new Date().toISOString(),
        items: Array.from({ length: 3 }, (_, i) => ({
          id: i + 1,
          name: `Item ${i + 1}`,
          value: Math.random() * 100,
        })),
      };

      this.setState({ data, loading: false });

      // Dispatch custom event
      this.dispatchEvent(
        new CustomEvent('data-loaded', {
          detail: data,
          bubbles: true,
          composed: true,
        })
      );
    } catch (error) {
      this.setState({
        error: error.message,
        loading: false,
      });
    }
  }

  clearData() {
    this.setState({
      data: null,
      error: null,
    });
  }

  setState(newState) {
    const oldState = { ...this.state };
    this.state = { ...this.state, ...newState };

    // Re-render content area
    const content = this.shadowRoot.getElementById('content');
    if (content) {
      content.innerHTML = this.renderContent();
    }

    // Dispatch state change event
    this.dispatchEvent(
      new CustomEvent('state-change', {
        detail: { oldState, newState: this.state },
        bubbles: true,
      })
    );
  }

  handleAttributeChange(name, value) {
    switch (name) {
      case 'data-source':
        if (this.state.connected) {
          this.loadData();
        }
        break;
      case 'auto-refresh':
        if (value !== null) {
          this.setupAutoRefresh();
        }
        break;
      case 'refresh-interval':
        // Restart auto-refresh with new interval
        this.cleanup();
        if (this.hasAttribute('auto-refresh')) {
          this.setupAutoRefresh();
        }
        break;
    }
  }

  cleanup() {
    // Clean up all subscriptions
    this.state.subscriptions.forEach((unsubscribe) => {
      try {
        unsubscribe();
      } catch (error) {
        console.error('Error during cleanup:', error);
      }
    });
    this.state.subscriptions = [];
  }

  // Public API
  refresh() {
    return this.loadData();
  }

  getState() {
    return { ...this.state };
  }

  getData() {
    return this.state.data;
  }
}

// Register the component
customElements.define('advanced-lifecycle', AdvancedLifecycleComponent);

// Usage examples
document.body.innerHTML += `
  <advanced-lifecycle 
    data-source="api/users" 
    auto-refresh 
    refresh-interval="3000">
  </advanced-lifecycle>
  
  <script>
    // Example of interacting with the component
    const component = document.querySelector('advanced-lifecycle');
    
    component.addEventListener('data-loaded', (event) => {
      console.log('Data loaded:', event.detail);
    });
    
    component.addEventListener('state-change', (event) => {
      console.log('State changed:', event.detail);
    });
    
    // Change attributes programmatically
    setTimeout(() => {
      component.setAttribute('data-source', 'api/posts');
    }, 10000);
  </script>
`;

Conclusion

Web Components provide a powerful way to create reusable, encapsulated UI elements using standard web technologies. By leveraging Custom Elements, Shadow DOM, and HTML Templates, you can build component libraries that work across different frameworks and applications. The key benefits include true encapsulation, reusability, and framework independence, making Web Components an excellent choice for building design systems and shared component libraries.

When building Web Components, focus on creating clear APIs, handling lifecycle events properly, and implementing robust event communication patterns. This ensures your components are maintainable, performant, and easy to use across different contexts.