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.

Web Components consist of four main technologies:
  • 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.

Shadow DOM structure

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>
Click Me

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, 2025

Slot 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, not my-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

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