DEV Community

Cover image for What Are Web Components, and Why Use Them? A React Example
Bryan Primus Lumbantobing
Bryan Primus Lumbantobing

Posted on • Edited on

What Are Web Components, and Why Use Them? A React Example

What Are Web Components?

Let's start with the definition of Web Components.

Based on webcomponents.org:

Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets built on Web Component standards work across modern browsers and can be used with any JavaScript library or framework that works with HTML.

From this definition, we know that using web components allows us to create reusable components for any project that uses HTML. Web components are essentially a set of custom, self-contained HTML tags, not tied to any specific library or framework.


Why Does This Matter?

Let's say your company runs multiple web applications. One is built in React, one in Vue, and another in plain HTML. Your design team wants a consistent <app-button> that looks and behaves the same everywhere.

With a framework component, you'd have to rebuild it three times, once per stack. With a web component, you build it once and it just works everywhere. That's the core value.

A web component works the same way in a React app, a Vue app, or no framework at all.

This matters especially if you're building a shared UI library, such as a design system, a component kit, or any set of elements meant to be consumed across different teams or projects. Web components let you ship those elements without forcing everyone to adopt the same framework.

Real companies have taken this approach. GitHub started small, creating components like <relative-time> and <local-time>, then gradually scaled up to more complex ones like <markdown-toolbar-element>. Web components gave them modularity and reusability, letting them build portable elements that fit into their existing codebase without committing to any specific framework.


The Three Building Blocks

Web components are built on three browser APIs that work together:

Custom Elements define what your tag does. You create a class, register it under a tag name, and the browser treats it like any built-in HTML element.

Shadow DOM gives your component its own isolated DOM tree. Styles defined inside it won't leak out to the rest of the page, and global styles won't bleed in either. Shadow DOM fixes CSS and DOM scoping problems. Without any extra tools or naming conventions, you can bundle CSS with markup, hide implementation details, and author self-contained components in vanilla JavaScript.

HTML Templates let you define markup with <template> and <slot> that is parsed but not rendered until you explicitly use it, making it efficient to reuse structure across multiple instances.

These three work independently, but combining them is where you get true encapsulation.


Building a Web Component from Scratch

Let's build a minimal <user-greeting> component that accepts a name attribute and renders a styled greeting. This walks through the whole flow end to end.

Step 1: Define the component file (user-greeting.js)

class UserGreeting extends HTMLElement {
  connectedCallback() {
    // 1. Create a shadow root to isolate this component's DOM and styles
    const shadow = this.attachShadow({ mode: 'open' });

    // 2. Read the attribute passed in from HTML
    const name = this.getAttribute('name') || 'stranger';

    // 3. Build the internal structure
    shadow.innerHTML = `
      <style>
        p {
          font-family: sans-serif;
          color: #333;
          background: #f0f4ff;
          padding: 8px 12px;
          border-radius: 6px;
          display: inline-block;
        }
      </style>
      <p>Hello, ${name}!</p>
    `;
  }
}

// 4. Register the tag name
customElements.define('user-greeting', UserGreeting);
Enter fullscreen mode Exit fullscreen mode

Step 2: Use it in HTML

<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="./user-greeting.js"></script>
  </head>
  <body>
    <user-greeting name="Ada"></user-greeting>
    <user-greeting name="Grace"></user-greeting>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

That's it. Both elements render independently with the same component. The <style> inside the shadow DOM only affects what's inside it and won't touch anything else on the page.

Note two things here: the tag name must contain a hyphen (like user-greeting) to distinguish custom elements from built-in ones, and you register it once with customElements.define().


Lifecycle Callbacks

Web components expose lifecycle callbacks you can hook into:

  • connectedCallback: Runs when the element is inserted into the DOM. This is where you typically do your setup.
  • disconnectedCallback: Runs when the element is removed from the DOM. Good for cleanup.
  • attributeChangedCallback: Runs when one of the element's observed attributes changes. Requires you to declare which attributes to watch via static get observedAttributes().
  • adoptedCallback: Runs when the element is moved to a new document.

If you want the component to react when the name attribute changes, here's how you wire that up:

class UserGreeting extends HTMLElement {
  static get observedAttributes() {
    return ['name']; // declare what to watch
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    this.render(); // re-render when attribute changes
  }

  render() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
    }
    const name = this.getAttribute('name') || 'stranger';
    this.shadowRoot.innerHTML = `<p>Hello, ${name}!</p>`;
  }
}

customElements.define('user-greeting', UserGreeting);
Enter fullscreen mode Exit fullscreen mode

Using a Web Component Inside React

Because React renders to HTML, you can drop any web component directly into JSX.

function App() {
  return (
    <div>
      <h1>Welcome</h1>
      <user-greeting name="Ada"></user-greeting>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

One thing to note: when using a web component inside JSX, you use class instead of className. This is because web components are native HTML elements, not React elements, and className is a JSX-specific convention for React's own DOM handling.

// React element: use className
<div className="card">...</div>

// Web component (native HTML element): use class
<user-greeting class="card" name="Ada"></user-greeting>
Enter fullscreen mode Exit fullscreen mode

Also worth knowing: prior to React 19, custom events from web components didn't propagate through React's event system automatically, so you had to attach them via addEventListener manually. React 19 adds native support for custom events, allowing developers to add event listeners using the familiar on + CustomEventName convention, just like standard events such as onClick.


Using React Inside a Web Component

You can also go the other direction and render a React component inside a web component. This is useful when migrating an existing React app toward a web component based architecture.

import ReactDOM from 'react-dom/client';

class SearchWidget extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('span');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);

    const query = this.getAttribute('query');
    const url = 'https://www.google.com/search?q=' + encodeURIComponent(query);

    // Render a React component inside the shadow DOM
    const root = ReactDOM.createRoot(mountPoint);
    root.render(<a href={url}>{query}</a>);
  }
}

customElements.define('search-widget', SearchWidget);
Enter fullscreen mode Exit fullscreen mode

Example adapted from the React documentation

This is then usable in plain HTML like any other custom element:

<search-widget query="web components"></search-widget>
Enter fullscreen mode Exit fullscreen mode

A Note on Tooling

Web components work in all major browsers without any framework or build tool. For simple cases, a <script type="module"> pointing to your component file is all you need.

That said, when you're building a larger project or integrating with React, a bundler like Webpack or Vite is recommended, especially if you're writing JSX inside a web component as in the example above.

If you find the raw API verbose, libraries like Lit offer a lightweight layer on top that adds reactive properties and template rendering while keeping you on web standards.


Summary

Web components solve one specific problem really well: write once, use anywhere. They're not a replacement for React or Vue, but they work alongside them. The real advantage shows up when you're building something meant to be shared across different applications or frameworks, such as a design system, a utility widget, or a standalone UI element that needs to outlive any single framework choice.

Next: Try building a small shared component with web components, or explore Lit for a more ergonomic authoring experience.


Further Reading

Top comments (0)