Web Components adoption guide: Overview, examples, and alternatives - LogRocket Blog (2024)

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 adoption guide: Overview, examples, and alternatives - LogRocket Blog (1)

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:

Web Components adoption guide: Overview, examples, and alternatives - LogRocket Blog (2)

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:

  • Create reusable components with Mitosis and Builder.io
  • Building reusable web components with Stencil.js
  • Credit card form web component tutorial
  • Build an off-canvas menu with <dialog> and web components

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>

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.jsclass 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' elementcustomElements.define('custom-card', CardComponent);

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>

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:

  • Nesting web components in vanilla JavaScript
  • Building web components with WebC in vanilla JavaScript

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:

Web Components adoption guide: Overview, examples, and alternatives - LogRocket Blog (5)

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 }}

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}

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}

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');}

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 }}

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);

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

More great articles from LogRocket:

  • Don't miss a moment with The Replay, a curated newsletter from LogRocket
  • Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
  • Use React's useEffect to optimize your application's performance
  • Switch between multiple versions of Node
  • Discover how to use the React children prop with TypeScript
  • Explore creating a custom mouse cursor with CSS
  • Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

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' });

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>

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>

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>

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>

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>

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);

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);});

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 }});

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 */}

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 */}

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 */}

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:

FeatureWeb ComponentsReactVueAngularSvelte
StandardizationW3C StandardLibraryFrameworkFrameworkCompiler
PerformanceNative efficiency, minimal overheadVirtual DOM, optimized updatesVirtual DOM, runtime optimizationAhead-of-Time compilation, dynamic loadingCompile-time optimization to vanilla JS
State ManagementExternal libraries or custom solutionsContext API, ReduxVuex, Composition APIRxJS, NgRxStores, Context API
ReactivityManual or library-assistedDeclarative UI, hooksDeclarative rendering, options/composition APITwo-way binding, observablesReactive declarations, assignments
RoutingRequires external librariesReact RouterVue RouterAngular RouterSvelteKit (or external libraries)
Data BindingProperty and event bindingOne-way data flowTwo-way data binding (v-model)Two-way binding (ngModel)Two-way binding, reactive statements
Framework IntegrationHighly interoperableCan integrate via wrappersDirect integration possibleDirect integration possibleDirect 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:

  • Shoelace: Build web components for React
  • Web components vs. React
  • Build web components with Svelte

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 getan app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, notserver-side

    • npm
    • Script tag
    $ npm i --save logrocket // Code:import LogRocket from 'logrocket'; LogRocket.init('app/id'); 
    // Add to your HTML:<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script><script>window.LogRocket && window.LogRocket.init('app/id');</script> 
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin

Get started now

Web Components adoption guide: Overview, examples, and alternatives - LogRocket Blog (2024)

References

Top Articles
Latest Posts
Article information

Author: Kerri Lueilwitz

Last Updated:

Views: 6073

Rating: 4.7 / 5 (47 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Kerri Lueilwitz

Birthday: 1992-10-31

Address: Suite 878 3699 Chantelle Roads, Colebury, NC 68599

Phone: +6111989609516

Job: Chief Farming Manager

Hobby: Mycology, Stone skipping, Dowsing, Whittling, Taxidermy, Sand art, Roller skating

Introduction: My name is Kerri Lueilwitz, I am a courageous, gentle, quaint, thankful, outstanding, brave, vast person who loves writing and wants to share my knowledge and understanding with you.