Web Components

Web Components are a set of web platform APIs that allow you to create custom, reusable, encapsulated HTML elements. They are built on web standards and work across modern browsers, providing a way to create components that can be used in any HTML page.

Introduction to Web Components

Web Components consist of four main technologies:

  • Custom Elements: JavaScript APIs for defining custom HTML elements
  • Shadow DOM: Encapsulated DOM and styling, separate from the main document DOM
  • HTML Templates: HTML fragments that can be cloned and inserted into the document
  • ES Modules: The JavaScript module system used for sharing components

Note: Web Components are supported in all major browsers including Chrome, Firefox, Safari, and Edge.

Custom Elements

Custom Elements allow you to define your own HTML elements with custom behavior:

// Define a custom element
class HelloWorld extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = `

Hello, World!

`; } } // Register the custom element customElements.define('hello-world', HelloWorld); // Now you can use it in HTML // <hello-world></hello-world>

Lifecycle Callbacks

Custom elements have 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', 'color'];
  }
}

customElements.define('my-element', MyElement);

Shadow DOM

Shadow DOM provides encapsulation for DOM and CSS, keeping the component's internal structure separate from the main document:

class UserCard extends HTMLElement {
  constructor() {
    super();
    
    // Create a shadow root
    const shadow = this.attachShadow({ mode: 'open' });
    
    // Create element
    const wrapper = document.createElement('div');
    wrapper.setAttribute('class', 'user-card');
    
    // Add styles
    const style = document.createElement('style');
    style.textContent = `
      .user-card {
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 16px;
        margin: 16px;
        font-family: sans-serif;
      }
      .user-name {
        font-weight: bold;
        color: #2c3e50;
      }
    `;
    
    // Get attribute values
    const name = this.getAttribute('name') || 'Anonymous';
    
    // Create the inner content
    wrapper.innerHTML = `
      
${name}
`; // Attach the created elements to the shadow DOM shadow.appendChild(style); shadow.appendChild(wrapper); } } customElements.define('user-card', UserCard); // Usage: // // john@example.com // 123-456-7890 //

Note: The mode: 'open' option makes the shadow root accessible from JavaScript outside the component using the element.shadowRoot property. Using mode: 'closed' would hide the shadow root.

HTML Templates

HTML Templates allow you to define fragments of markup that can be cloned and used multiple times:

<!-- Define a template in HTML -->
<template id="product-template">
  <div class="product">
    <img class="product-image" />
    <h2 class="product-name"></h2>
    <p class="product-price"></p>
    <button class="buy-button">Buy Now</button>
  </div>
</template>

<script>
  class ProductCard extends HTMLElement {
    constructor() {
      super();
      
      // Create shadow DOM
      const shadow = this.attachShadow({ mode: 'open' });
      
      // Get the template content
      const template = document.getElementById('product-template');
      const templateContent = template.content;
      
      // Clone the template
      const clone = templateContent.cloneNode(true);
      
      // Customize the cloned template
      const image = clone.querySelector('.product-image');
      const name = clone.querySelector('.product-name');
      const price = clone.querySelector('.product-price');
      
      image.src = this.getAttribute('image') || '';
      name.textContent = this.getAttribute('name') || 'Product Name';
      price.textContent = `$${this.getAttribute('price') || '0.00'}`;
      
      // Add styles
      const style = document.createElement('style');
      style.textContent = `
        .product {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
          text-align: center;
        }
        .product-image {
          max-width: 100%;
          height: auto;
        }
        .buy-button {
          background-color: #4CAF50;
          color: white;
          border: none;
          padding: 8px 16px;
          border-radius: 4px;
          cursor: pointer;
        }
      `;
      
      // Attach the elements to the shadow DOM
      shadow.appendChild(style);
      shadow.appendChild(clone);
    }
  }
  
  customElements.define('product-card', ProductCard);
</script>

<!-- Usage -->
<product-card 
  name="Wireless Headphones" 
  price="99.99" 
  image="headphones.jpg">
</product-card>

Slots

Slots provide a way to compose components by allowing users to insert custom content:

class InfoCard extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      
      
Default Header
Default content
`; } } customElements.define('info-card', InfoCard); // Usage: // // Custom Header //

This is the main content.

// Custom Footer //

Attributes and Properties

Custom elements can react to changes in attributes and expose properties:

class ColoredBox extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.render();
  }
  
  // Define which attributes to observe
  static get observedAttributes() {
    return ['color', 'size'];
  }
  
  // Respond to attribute changes
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }
  
  // Getter for color property
  get color() {
    return this.getAttribute('color') || 'blue';
  }
  
  // Setter for color property
  set color(value) {
    this.setAttribute('color', value);
  }
  
  // Getter for size property
  get size() {
    return this.getAttribute('size') || '100px';
  }
  
  // Setter for size property
  set size(value) {
    this.setAttribute('size', value);
  }
  
  // Render the component
  render() {
    this.shadowRoot.innerHTML = `
      
      
`; } } customElements.define('colored-box', ColoredBox); // Usage: // Hello!

Events in Web Components

Web Components can dispatch and listen for events:

class CounterButton extends HTMLElement {
  constructor() {
    super();
    
    this._count = 0;
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      
      
      0
    `;
    
    this._button = shadow.querySelector('button');
    this._counter = shadow.querySelector('.counter');
    
    this._button.addEventListener('click', () => {
      this._count++;
      this._counter.textContent = this._count;
      
      // Dispatch a custom event
      this.dispatchEvent(new CustomEvent('counter-changed', {
        detail: { count: this._count },
        bubbles: true,
        composed: true // Allows the event to cross the shadow DOM boundary
      }));
    });
  }
}

customElements.define('counter-button', CounterButton);

// Usage:
// 
// 

Note: The composed: true option allows the event to propagate outside of the shadow DOM. Without this, events would be contained within the shadow DOM.

Styling Web Components

There are several ways to style Web Components:

Internal Styles

Styles defined within the shadow DOM only affect elements inside it:

class StyledComponent extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      
      

This text is styled by the component's internal CSS.

`; } } customElements.define('styled-component', StyledComponent);

External Styling with CSS Variables

CSS custom properties (variables) can penetrate the shadow DOM boundary:

class ThemeableComponent extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      
      
`; } } customElements.define('themeable-component', ThemeableComponent); // External CSS: // :root { // --theme-background: #2c3e50; // --theme-text: #ecf0f1; // --theme-padding: 24px; // --theme-border-radius: 8px; // --theme-font: 'Arial', sans-serif; // }

::part and ::slotted Selectors

These CSS selectors provide ways to style specific parts of web components:

class AdvancedComponent extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      
      
Default Header
Default Footer
`; } } customElements.define('advanced-component', AdvancedComponent); // External CSS: // advanced-component::part(header) { // background-color: #f0f0f0; // font-weight: bold; // padding: 8px; // } // // advanced-component::part(footer) { // font-style: italic; // margin-top: 16px; // }

Web Component Libraries

Several libraries exist to simplify Web Component development:

  • Lit: A lightweight library from Google for building Web Components
  • Stencil: A compiler that generates Web Components
  • Polymer: One of the first libraries for building Web Components
  • Hybrids: A UI library for creating Web Components with simple and functional API

Example with Lit

import { LitElement, html, css } from 'lit';

class SimpleGreeting extends LitElement {
  static properties = {
    name: { type: String }
  };

  static styles = css`
    p {
      color: blue;
      font-family: sans-serif;
    }
  `;

  constructor() {
    super();
    this.name = 'World';
  }

  render() {
    return html`

Hello, ${this.name}!

`; } } customElements.define('simple-greeting', SimpleGreeting); // Usage: //

Interactive Example

Try out this interactive example to see Web Components in action:

Best Practices

  • Follow the Custom Elements naming convention: Always include a hyphen in the name (e.g., my-element)
  • Keep components focused: Each component should have a single responsibility
  • Use Shadow DOM for encapsulation: This prevents style leakage and DOM conflicts
  • Provide a clean API: Use attributes, properties, and events for communication
  • Document your components: Make it clear how to use your components
  • Make components accessible: Ensure they work with keyboard navigation and screen readers

Next Steps

Now that you understand Web Components, you might want to explore:

  • Building a component library with Web Components
  • Using Web Components with frameworks like React, Vue, or Angular
  • Advanced Shadow DOM techniques
  • Web Component performance optimization
  • Testing Web Components