DEV Community

Cover image for Web Components adoption guide: Overview, examples, and alternatives
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Web Components adoption guide: Overview, examples, and alternatives

Written by Elijah Asaolu✏️

HTML, at its core, is limited to a specified collection of tags, each with its own functions. However, developers regularly face situations where they need to reuse certain blocks of code (HTML tags) across different sections of an application. This recurring requirement paved the way for the development of Web Components.

Web Components is a set of standards that allow developers to expand HTML with custom tags, encapsulating code in a reusable and manageable manner. In this guide, we'll look at web components, how they work, how to create them, and their benefits and drawbacks.

We will also compare Web Components to other popular JavaScript component frameworks, exploring their different advantages and potential use cases. Ultimately, this guide should help you thoroughly assess Web Components and determine whether to use it in your next project.


What is Web Components?

Web Components is a suite of modern web standards that allow developers to create reusable, encapsulated, and interoperable custom elements.

In other words, in addition to the default HTML elements, we can create a new markup, such as a dropdown or carousel component. Then, we can add our desired functionality with JavaScript, give it a custom tag name, and render it like any other HTML element.

To achieve all these, Web Components utilizes multiple technical standards behind the scenes — namely, custom elements, shadow DOM, HTML templates, and ES modules: Graphic Showing How Web Components Works Behind The Scenes, Listing Custom Elements, Shadow Dom, Html Templates, And Es Modules, Along With How Each Works

The combination of these technologies creates a comprehensive toolkit for developing complex, reusable web components:

  • Custom elements help to define component behavior and interfaces
  • The shadow DOM assures style and markup encapsulation
  • HTML templates provide a means for describing component structure
  • ES modules facilitate code organization and reuse

In subsequent sections, we'll go over these features in more detail.

Further reading:


Why use Web Components?

Web Components offers several key advantages for web development:

  • Performance: Utilizing the browser's native features makes Web Components lightweight and fast, reducing the need for additional libraries and decreasing load and runtime speeds. This efficiency is particularly beneficial for performance-critical applications
  • Developer experience: Thanks to their encapsulation, web components streamline development and maintenance. They enable a modular approach, allowing for isolated development, testing, and deployment. This, in turn, simplifies codebase management and enhances code clarity and scalability
  • Smaller bundle sizes: Since web components leverage native browser capabilities, using them often leads to reduced application bundle sizes. Without the need for large external libraries, applications load faster, improving the user experience
  • Framework-agnostic: You can integrate web components into any web framework or application, offering great flexibility and enabling their use across various projects without compatibility concerns

While Web Components offers numerous benefits, there are also some challenges to consider:

  • SEO and accessibility: Encapsulating web components with the shadow DOM can complicate SEO and accessibility, potentially hindering content visibility to search engines and assistive technologies
  • Tooling ecosystem: The tooling for Web Components is evolving but may lack the maturity and convenience of established frameworks, possibly leading to a steeper learning curve
  • Styling and theming: Ensuring consistent styling and theming across web components, particularly from different sources, requires a thoughtful strategy and possibly additional tools to overcome encapsulation boundaries

Nevertheless, you can manage or minimize most of these drawbacks effectively by using web components strategically.


Creating web components

To demonstrate Web Components in action, let's create a basic card component. First, we need to design the basic HTML structure for our card component, defining what it should look like:

<style>
  /* Styles for the card */
  .card {
    font-family: "Arial", sans-serif;
    background: #fff;
    width: 300px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    margin: 16px;
    border-radius: 10px;
    overflow: hidden;
  }
  .container {
    padding: 2px 16px;
  }
</style>
<div class="card">
  <img src="path/to/image" alt="Card Image" />
  <div class="container">
    <h4><b>Card Title</b></h4>
    <p>Card Description</p>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

With the markup finalized, we can now define our component in JavaScript. This entails creating a class that extends HTMLElement and calling the customElements.define() method, as shown below:

// app.js
class CardComponent extends HTMLElement {
    constructor() {
        super();
        // Attach a shadow DOM tree to this instance of the component
        const shadow = this.attachShadow({mode: 'open'});

        // Create a clone of the template content
        const template = document.createElement('template');
        template.innerHTML = `
            <style>
                /* Styles for the card */
                .card {
                    font-family: 'Arial', sans-serif;
                    background: #fff;
                    width: 300px;
                    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
                    margin: 16px;
                    border-radius: 10px;
                    overflow: hidden;
                }
                .container {
                    padding: 2px 16px;
                }
            </style>
            <div class="card">
                <img src="path/to/image" alt="Card Image">
                <div class="container">
                    <h4><b>Card Title</b></h4>
                    <p>Card Description</p>
                </div>
            </div>
        `;

        // Append the template content to the shadow DOM
        shadow.appendChild(template.content.cloneNode(true));
    }
}

// Define the 'custom-card' element
customElements.define('custom-card', CardComponent);
Enter fullscreen mode Exit fullscreen mode

In the code above, we've defined a class called CardComponent that extends HTMLElement. Within the constructor, we initiate a shadow DOM for encapsulation, allowing internal features of the component — like styles — to remain isolated from the rest of the document.

The template element defines our card's HTML structure and styling. This element is then cloned and appended to the shadow DOM, making it part of our custom element.

To use our newly created component, we simply need to import the JavaScript file where our component is defined into an HTML page. Then, we can use the custom tag as we would with any standard HTML element:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Custom Card Component</title>
    <script src="app.js"></script>
</head>
<body>
    <!-- Render the custom card component -->
    <custom-card></custom-card>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Following these steps, the custom <custom-card> element becomes recognized by the browser. When the page is loaded, the browser processes the tag, creates an instance of CardComponent, and renders it according to our definitions.

Further reading:


Key Web Components features to know

Web Components provides essential capabilities that allow us to create robust and easily maintainable components. Let's quickly go over some of them.

Lifecycle methods

Web Components includes many lifecycle callbacks that allow you to take specified actions at certain points in a component's lifespan. These callbacks are useful for managing resources, responding to changes, and ensuring that your components behave predictably: Graphic Showing How Various Web Components Lifecycle Callbacks Interact With Each Other

constructor

The constructor is called when an element instance is created or upgraded. It’s where you set up the initial state, default values, and event listeners:

class MyElement extends HTMLElement {
  constructor() {
    super(); // Always call super first in a constructor
    // Initialize state or bind event listeners
  }
}
Enter fullscreen mode Exit fullscreen mode

It’s recommended to utilize this method solely for initial setup. Avoid including any code that modifies attributes or children, since the element may not be fully set up.

connectedCallback

connectedCallback is invoked each time the custom element is appended to a document-connected element:

connectedCallback() {
  this.render(); // For example, a method to render component's HTML
}
Enter fullscreen mode Exit fullscreen mode

This callback is a good place to run setup code, such as fetching resources or rendering. Essentially, it's where you should prepare your component for display.

disconnectedCallback

The disconnectedCallback function is called when a custom element is disconnected from the document's DOM:

disconnectedCallback() {
  // Cleanup work
}
Enter fullscreen mode Exit fullscreen mode

Use this callback to handle cleanup operations, such as removing event listeners or canceling any ongoing network requests.

adoptedCallback

adoptedCallback is invoked each time the custom element is moved to a new document:

adoptedCallback() {
  console.log('Component moved to new document');
}
Enter fullscreen mode Exit fullscreen mode

This is a less commonly used lifecycle hook but can be useful for components that are moved between documents, such as in a multi-frame application.

attributeChangedCallback

This callback is invoked each time one of the custom element's attributes is added, removed, or changed. You must specify which attributes you want to observe using a static observedAttributes getter:

static get observedAttributes() { return ['disabled', 'label']; }

attributeChangedCallback(name, oldValue, newValue) {
  // Respond to attribute changes
  if (name === 'disabled') {
    // Handle the 'disabled' attribute change
  }
}
Enter fullscreen mode Exit fullscreen mode

This method is essential for responding to changes in attributes, enabling dynamic updates to the component's state or appearance in response to attribute modifications.

Simple demo usage of lifecycle methods

Putting everything together, here's a simple example of how these methods might be combined into a custom element definition:

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    // Initial setup
  }

  connectedCallback() {
    this.innerHTML = `<p>Hello, World!</p>`;
  }

  disconnectedCallback() {
    // Cleanup
  }

  adoptedCallback() {
    // Respond to moving document
  }

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

  attributeChangedCallback(name, oldValue, newValue) {
    // React to attribute changes
    if (name === 'example') {
      this.updateComponent(newValue);
    }
  }

  updateComponent(value) {
    // Update component based on attribute
  }
}

customElements.define('my-custom-element', MyCustomElement);
Enter fullscreen mode Exit fullscreen mode

With these lifecycle methods, you can fully utilize the Custom Elements API to develop dynamic, responsive, and efficient web components.

Shadow DOM

The shadow DOM allows you to include a subtree of DOM elements in a document, but with a separate scope for CSS and JavaScript. This means that styles and scripts defined in a shadow DOM will not leak into the main document, nor will external styles or scripts interfere with the shadow DOM's internals.

To create a shadow DOM, use the attachShadow method on an element, which returns a shadow root. The mode property can be set to either open or closed, indicating whether the shadow root is accessible from JavaScript outside the shadow DOM:

const element = document.createElement('div');
const shadowRoot = element.attachShadow({ mode: 'open' });
Enter fullscreen mode Exit fullscreen mode

In open mode, the shadow root is accessible from the main document via the element.shadowRoot property. However, in closed mode, the shadow root is not accessible from the main document, providing stronger encapsulation.

Template slots

Slots are defined in the shadow DOM of a web component and serve as placeholders for any user-supplied content. When a component is used in an HTML document, items within the custom element can be assigned to specific slots using the slot attribute, allowing for flexible content organization within custom elements.

Here's a simplified example to illustrate the concept:

<!-- Inside your web component's template -->
<div>
  <slot name="custom-content"></slot>
</div>
Enter fullscreen mode Exit fullscreen mode

In this example, we defined a <slot> tag with the name custom-content, indicating where the user-defined content should be placed within the component. We can now assign content to this slot, as shown below.

<!-- Usage in an HTML document -->
<your-custom-element>
  <div slot="custom-content">This is custom content for the slot.</div>
</your-custom-element>
Enter fullscreen mode Exit fullscreen mode

Default content

Slots allow for default content, which is displayed inside the slot if no content is assigned to it. This feature is particularly useful for providing fallback content or instructions within custom elements:

<template>
  <slot name="user-greeting">Hello, guest!</slot>
</template>
Enter fullscreen mode Exit fullscreen mode

In this example, the default content — Hello, guest! — is displayed if no content is provided to the user-greeting slot. This enhances the usability of the component by ensuring that it always has meaningful content.

Named slots

Named slots enable a component to define multiple distinct slots, each intended for different content. This allows for a more structured and semantic distribution of content within a component:

<template>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot name="main-content"></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</template>
Enter fullscreen mode Exit fullscreen mode

As described in the example above, named slots allow you to carefully control your component content layout by defining slots for the header, main content, and footer.

Fallback content

Fallback content is similar to default content, but is specifically designed to be shown in case of errors or when the expected content cannot be loaded or is unavailable:

<template>
  <slot name="profile-picture">
    <img src="default-profile.jpg" alt="Default Profile Picture">
  </slot>
</template>
Enter fullscreen mode Exit fullscreen mode

This slot provides a default profile picture as fallback content, ensuring the component always has a visual representation, even if the user does not provide a specific image.

Events

Events in Web Components facilitate component-to-component communication and enable custom notifications. These events can include custom data and may either bubble up through the DOM or be restricted to the originating component.

Creating events involves specifying several options:

  • bubbles: When true, this allows the event to bubble up through the DOM
  • composed: When true, the event can cross the shadow DOM boundary to reach the regular DOM, making it accessible outside the component
  • cancelable: Enables the event to be canceled, providing a way to signal that an action associated with the event should not proceed
  • detail: Allows the inclusion of custom data within the event

To emit a custom event, we can use the CustomEvent constructor. This constructor extends the standard Event interface, enabling us to include custom data via the detail property.

Here's an example of how a custom event might be dispatched from a web component:

class MyComponent extends HTMLElement {
  constructor() {
    super();
    // Example of dispatching a custom event
    const event = new CustomEvent('my-custom-event', {
      detail: { /* Custom data */ },
      bubbles: true, // Allows the event to bubble up through the DOM
      composed: true // Enables the event to cross the shadow DOM boundary
    });
    this.dispatchEvent(event);
  }
}

window.customElements.define('my-component', MyComponent);
Enter fullscreen mode Exit fullscreen mode

In this example, we dispatched a custom event named my-custom-event that has the potential to carry custom data.

Web components can also listen to events using the addEventListener method:

document.addEventListener('my-custom-event', (e) => {
  console.log('Custom event received:', e.detail);
});
Enter fullscreen mode Exit fullscreen mode

Another important feature is event delegation, a technique that leverages event bubbling to listen for events at a higher level than their dispatch source. It's useful within Web Components for managing events from multiple child elements without needing individual listeners for each one:

this.addEventListener('click', (event) => {
  if (event.target.matches('.some-selector')) {
    // Specific event handling
  }
});
Enter fullscreen mode Exit fullscreen mode

Using event delegation simplifies handling events for components with many child elements or for dynamically added elements, enhancing efficiency and maintainability.


Styling web components

When you define a component, you can include a <style> tag within its shadow root to apply CSS rules that affect only the component's internal elements.

However, this encapsulation is a double-edged sword: it keeps the component's styles isolated, but it also blocks external styles from affecting the component's internals directly. For example, global styles defined outside a web component will not penetrate the shadow DOM.

Let’s discuss the following best practices for addressing this issue.

Utilizing CSS variables for flexible styling

CSS variables are essential for effectively styling web components. They have the unique ability to cross the shadow DOM boundary, making them ideal for defining a component's customizable aspects like colors, fonts, and spacing. Here's how you can leverage them:

/* Within your web component */
:host {
  --button-bg-color: blue; /* Background color variable */
  --button-text-color: white; /* Text color variable */
  display: block;
}

button {
  background-color: var(--button-bg-color); /* Use the background color variable */
  color: var(--button-text-color); /* Use the text color variable */
}
Enter fullscreen mode Exit fullscreen mode

Setting these variables allows users of your component to quickly adjust them to apply their desired global themes or specific styles, all while respecting the encapsulation boundary.

Leveraging ::part for targeted styling

The ::part pseudo-element offers a powerful way to expose specific elements within your component's shadow DOM for external styling. Mark elements with the part attribute to make them available to style from outside the component:

/* Inside your web component */
button.part {
  /* Base styling */
}

/* In an external global stylesheet */
my-component::part(button) {
  /* Overrides or additional styling */
}
Enter fullscreen mode Exit fullscreen mode

This method strikes a perfect balance, maintaining style encapsulation while allowing for precise customization of component parts.

Integrating global styles

You can include a global stylesheet with CSS custom properties or classes to apply global styles or themes to web components. This approach ensures a unified aesthetic across your application without compromising the encapsulated styles of individual components:

/* Global stylesheet */
:root {
  --primary-color: green; /* Define a global primary color */
  --secondary-color: orange; /* Define a global secondary color */
}
Enter fullscreen mode Exit fullscreen mode

Your components can then leverage these global styles, adopting application-wide themes while retaining their own encapsulated styles. This method improves your app’s uniformity and modularity.


Web Components vs. component frameworks

Some popular component-based frameworks and libraries include React, Vue, Angular, and Svelte. When choosing between these options, it's essential to compare them based on features, performance, community support, and documentation.

Web components often outperform frameworks because they use native browser APIs, reducing overhead and potentially speeding up load times. In comparison, Svelte enhances runtime efficiency by compiling to optimized JavaScript, while React and Vue use virtual DOM for updates.

When it comes to features, Web Components excels at creating reusable, encapsulated elements, sticking closely to web standards. Frameworks offer broader ecosystems, including state management and routing, beyond mere component models.

The community size and resource availability are where web components fall short in comparison to the frameworks and libraries mentioned. Web Components has a smaller but growing community, whereas React, Vue, Angular, and Svelte each boast large, established communities with extensive resources and documentation, offering greater support for complex projects.

As usual, the choice between Web Components and other component-based frameworks and libraries depends on your project's needs and preferred features.

Web components are ideal for creating interoperable, standards-compliant components. In contrast, the component frameworks and libraries we’ve discussed offer a richer set of features and stronger community support for building full-scale web applications.

Below is a summary table that highlights the comparative aspects of these technologies:

Feature Web Components React Vue Angular Svelte
Standardization W3C Standard Library Framework Framework Compiler
Performance Native efficiency, minimal overhead Virtual DOM, optimized updates Virtual DOM, runtime optimization Ahead-of-Time compilation, dynamic loading Compile-time optimization to vanilla JS
State Management External libraries or custom solutions Context API, Redux Vuex, Composition API RxJS, NgRx Stores, Context API
Reactivity Manual or library-assisted Declarative UI, hooks Declarative rendering, options/composition API Two-way binding, observables Reactive declarations, assignments
Routing Requires external libraries React Router Vue Router Angular Router SvelteKit (or external libraries)
Data Binding Property and event binding One-way data flow Two-way data binding (v-model) Two-way binding (ngModel) Two-way binding, reactive statements
Framework Integration Highly interoperable Can integrate via wrappers Direct integration possible Direct integration possible Direct integration possible

This table reflects the strengths and limitations of each technology, aiding in making an informed decision based on the specific needs of a project or development team.

Further reading:


Conclusion

In this adoption guide, we’ve explored how Web Components works behind the scenes. We discussed how to get started with creating web components, their major benefits and drawbacks, as well as how they compare with other JavaScript component frameworks and libraries.


Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (1)

Collapse
 
dannyengelman profile image
Danny Engelman