Part 1/2: Introduction to Web Components and Cross-Framework Integration
Exploring the Foundations of Web Components and Integrating Them into React
Table of contents
- Introduction to Web Components
- What are Web Components?
- Key Technologies Behind Web Components
- Why Use Web Components?
- Understanding Web Components in Multi-Tech Stack Projects
- Integrating Web Components into React Applications
- How Web Component Registration Works
- Why Do You Still Need to Import Web Components in React?
- Using Web Components in React: Practical Example
- Handling Web Component Attributes in React
- Setting Attributes with React Refs
- Example: Passing React State to Web Components
- Handling Events Between Web Components and React
- Listening to Custom Events in React Using Refs
- Example: Two-Way Data Binding Between React and Web Components
- Conclusion for Part 1
Disclaimer!
This 2-part series is not meant for a quick read—it requires you to take the time to analyze the code blocks and connect the functionality as you read through. Since Web Components is a distributed concept, it assumes some prior understanding of important concepts like Events, Markup, bundlers, and HTML element attributes. By diving into the details, this series aims to set a solid foundation for you to start thinking about solutions with Web Components, enabling you to confidently use them in your own projects.
Introduction to Web Components
As front-end development continues to evolve, we’re constantly on the lookout for better ways to create reusable, maintainable, and scalable user interfaces. One of the most powerful tools in the modern web developer’s toolbox is Web Components.
Web Components allow us to build custom HTML elements that encapsulate their structure, styling, and behavior. These elements can be reused across projects, frameworks, and even different tech stacks without worrying about how they’re implemented under the hood. Whether you're working with React, Angular, Vue, or plain HTML/JS, Web Components give you the flexibility to create truly framework-agnostic, reusable components.
Let’s dive into what makes Web Components so special, starting with their key technologies.
What are Web Components?
At its core, a Web Component is a custom, reusable element that behaves just like any other HTML element. The difference is that you define what this element looks like, how it behaves, and how it interacts with other elements. You can think of Web Components as a combination of three key technologies:
Custom Elements – Define your own HTML elements with specific behavior.
Shadow DOM – Encapsulate the internal structure and styles of the element, so they don’t interfere with the rest of the page.
HTML Templates and Slots – Provide reusable markup and allow for flexible content insertion.
Each of these technologies works together to create a truly isolated, reusable, and powerful component system.
Key Technologies Behind Web Components
1. Custom Elements
At the heart of Web Components are Custom Elements. These are exactly what they sound like: elements you create yourself. You can define new HTML tags (like <custom-button>
) and give them specific functionality, just like how you use <button>
or <input>
.
Here’s how you can define your own custom element:
class MyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `<button>Click Me!</button>`;
}
}
customElements.define('my-button', MyButton);
Now you can use <my-button>
anywhere in your HTML, and it will render a button:
<my-button></my-button>
This simple example might look underwhelming, but when combined with the Shadow DOM and HTML templates, you can create fully-featured, isolated components that are reusable across projects and frameworks.
2. Shadow DOM
One of the trickiest things about web development is managing styles across large projects. Have you ever styled a button in one part of your app only to find that it’s broken elsewhere because of some conflicting CSS? This is where the Shadow DOM comes in.
The Shadow DOM lets you encapsulate your component’s structure and styles so they don’t affect anything outside of the component, and nothing outside can affect your component either. Think of it like an impenetrable barrier around your component’s inner workings.
Here’s how we add a Shadow DOM to our MyButton
component:
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
background-color: purple;
color: white;
padding: 10px;
border: none;
border-radius: 5px;
}
</style>
<button>Click Me!</button>
`;
}
}
customElements.define('my-button', MyButton);
Now, the button inside <my-button>
has its own style, and those styles won’t affect or be affected by any other part of your app.
Illustration:
Without Shadow DOM, you could have a button with global styles that override its appearance.
With Shadow DOM, the button’s styles are isolated. No external CSS can break the button’s design, and your component behaves consistently everywhere.
3. HTML Templates and Slots
Sometimes you need to create reusable pieces of markup within your custom elements. That’s where HTML templates come into play. Templates allow you to define reusable chunks of HTML that are only rendered when needed, rather than at the moment the page is loaded.
Here’s an example:
<template id="my-template">
<style>
p {
color: red;
}
</style>
<p>I'm inside a template!</p>
</template>
<script>
class MyTemplate extends HTMLElement {
constructor() {
super();
const template = document.getElementById('my-template');
const templateContent = template.content.cloneNode(true);
this.attachShadow({ mode: 'open' }).appendChild(templateContent);
}
}
customElements.define('my-template', MyTemplate);
</script>
This <template>
tag defines some HTML (a red paragraph in this case) that can be used inside your custom element without rendering it directly in the document. When the component is created, the content from the template is "stamped out" into the Shadow DOM.
Slots: Customizable Content
Sometimes you need part of a Web Component to be customizable. This is where slots come in. Slots allow you to insert content from outside the component into specific parts of its template.
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
}
</style>
<div class="card">
<slot name="title"></slot>
<p><slot></slot></p>
</div>
`;
}
}
customElements.define('my-card', MyCard);
Here’s how you can use slots:
<my-card>
<h3 slot="title">Card Title</h3>
This is the content inside the card.
</my-card>
The title slot allows for the <h3>
element to be inserted from the outside, while the default slot lets us inject other content inside the <p>
tag. This flexibility is powerful when building reusable UI components.
Why Use Web Components?
Now that we’ve gone over the basic building blocks of Web Components, you might be asking: Why should I bother with all this? Why not just stick with React, Angular, or Vue?
Here’s why Web Components are awesome:
Framework Agnostic: Web Components work with any JavaScript framework—or none at all. You can use them in React, Angular, Vue, or even in plain HTML/JavaScript. This makes them an ideal choice for projects with multiple tech stacks.
Encapsulation: Thanks to the Shadow DOM, your components' styles and structure are completely isolated. You never have to worry about global CSS breaking your component—or vice versa.
Reusability: Build a component once and reuse it across multiple projects, no matter the framework. This drastically reduces code duplication and ensures consistency across large applications.
Future-Proof: Web Components are part of the browser’s native APIs. They’re not tied to any framework that might go out of fashion. Your custom elements will continue to work for years to come.
Great for Microfrontends: If you're working in a large team with different tech stacks (e.g., React in one section, Angular in another), Web Components allow you to share UI elements across all parts of the app without having to rewrite them for each framework.
Understanding Web Components in Multi-Tech Stack Projects
Building and Sharing the MyButton
Web Component
Let’s go through how you can create a simple Web Component, bundle it into a UMD module, and share it for use in various frameworks like React, Angular, and Vue. We'll also make sure it's clear where the bundle needs to be placed and how it can be imported and used.
Step 1: Creating the MyButton
Component
We start by defining the Web Component in a file called MyButton.js
:
// MyButton.js
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
background-color: var(--button-bg-color, #007BFF);
color: var(--button-text-color, #ffffff);
padding: var(--button-padding, 10px 20px);
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: var(--button-hover-bg-color, #0056b3);
}
</style>
<button><slot></slot></button> <!-- Slot for dynamic content -->
`;
}
}
customElements.define('my-button', MyButton);
This component:
Encapsulates its styles using the Shadow DOM.
Uses CSS custom properties to allow customization (e.g.,
--button-bg-color
,--button-padding
).Uses a slot to insert dynamic content inside the button.
Step 2: Bundling the Web Component as a UMD Module
To make this component reusable across projects, you can bundle it using Webpack or Rollup into a UMD (Universal Module Definition) format. The UMD format ensures that the component can be imported in different environments (e.g., via a <script>
tag or as a module in frameworks).
Here’s the Webpack configuration to bundle the MyButton.js
component:
Webpack configuration (webpack.config.js
):
const path = require('path');
module.exports = {
entry: './MyButton.js', // Entry point for the Web Component
output: {
filename: 'my-button.bundle.js', // Name of the bundled output file
path: path.resolve(__dirname, 'dist'), // Output directory for the bundle
library: 'MyButton', // Optional global variable name
libraryTarget: 'umd', // UMD format to work in different environments
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader', // Transpile ES6+ code for compatibility
},
},
],
},
};
After configuring Webpack, you can generate the bundle by running:
npx webpack --config webpack.config.js
This will create a bundled file called my-button.bundle.js
inside a dist
directory. This file contains your MyButton
component in a UMD format, ready to be used in any project.
Step 3: Where to Place the UMD Bundle
Once you have generated the UMD bundle (my-button.bundle.js
), you have several options for how to distribute and use it:
Local Projects:
Place the
my-button.bundle.js
file in your project’spublic
orassets
folder.You can then load it via a
<script>
tag directly in your HTML files or import it into your JavaScript modules.
Sharing Across Teams:
Option 1: Publish to npm: If you want other teams to be able to install the Web Component via a package manager, publish it to npm.
npm publish
Other teams can then install it via:
npm install my-button
Option 2: Host the Bundle on a CDN: You can upload the bundle to a CDN (like jsDelivr or unpkg) so that it can be loaded directly into any project via a
<script>
tag. For example:<script src="https://cdn.example.com/my-button.bundle.js"></script>
Using the Bundle in Different Frameworks: Once the bundle is published or placed in a project's public directory, teams can use it by either importing it in JavaScript files or referencing it through a script tag.
Using the MyButton
Component Across Different Frameworks
Now that we have the UMD bundle (my-button.bundle.js
), let’s see how it can be used in React, Angular, and Vue.
1. Using MyButton
in React
If you’ve published the component to npm, or placed the my-button.bundle.js
in your project’s public
directory, you can import it into your React application like this:
First, install the component (if published to npm):
npm install my-button
Alternatively, if you’re using the my-button.bundle.js
locally, make sure it’s placed in your project’s public
directory, and import it directly into your React component:
import React, { useRef, useEffect } from 'react';
import 'my-button/dist/my-button.bundle.js'; // Import the Web Component
function App() {
const buttonRef = useRef(null);
useEffect(() => {
// Attach an event listener for button click
buttonRef.current.addEventListener('click', () => {
alert('Button clicked!');
});
}, []);
return (
<div>
<h1>Using Web Components in React</h1>
{/* Use the custom button with customizable styles */}
<my-button ref={buttonRef} style="--button-bg-color: #28a745;">
Click Me
</my-button>
</div>
);
}
export default App;
In this case:
You import the bundled Web Component (
my-button.bundle.js
) and then use it in your React component.You can pass CSS custom properties to customize the button’s appearance (e.g.,
--button-bg-color: #28a745
).Use refs to access the Web Component and add event listeners (like handling button clicks).
2. Using MyButton
in Angular
For Angular, you’ll need to allow Angular to recognize custom elements by adding CUSTOM_ELEMENTS_SCHEMA
to your module’s schema.
Step 1: Install the Web Component
If the component is published to npm:
npm install my-button
Alternatively, if you’re using the local UMD bundle, place my-button.bundle.js
in your project’s src/assets
directory and load it with a <script>
tag in your index.html file:
<!-- src/index.html -->
<script src="assets/my-button.bundle.js"></script>
Step 2: Update Your Angular Module
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Allow custom elements
bootstrap: [AppComponent]
})
export class AppModule { }
Step 3: Use the Web Component in Angular Templates
<!-- app.component.html -->
<my-button style="--button-bg-color: #dc3545;">
Delete
</my-button>
3. Using MyButton
in Vue
In Vue, you can directly use Web Components in your templates. Just import the UMD bundle and reference the component in your Vue component.
Step 1: Install or Reference the UMD Bundle
If using npm:
npm install my-button
If you have the local bundle, place it in the
public
folder and include it in index.html:<!-- index.html --> <script src="public/my-button.bundle.js"></script>
Step 2: Use MyButton
in Vue
<template>
<div>
<h1>Using Web Components in Vue</h1>
<my-button style="--button-bg-color: #ffc107;">
Warning
</my-button>
</div>
</template>
<script>
import 'my-button/dist/my-button.bundle.js'; // Import the Web Component
export default {
name: 'App'
};
</script>
Handling Customization with CSS Custom Properties
CSS custom properties (CSS variables) make it easy to allow teams to customize your Web Component without modifying its internal code. In our MyButton
component, you can control the button’s background color, text color, padding, and hover effect using these properties.
For example, in any framework, you can use the Web Component like this:
<my-button style="--button-bg-color: #007bff; --button-text-color: white;">
Submit
</my-button>
This approach lets teams adapt the button to match their own style guides while ensuring the underlying logic remains consistent across projects.
Integrating Web Components into React Applications
Although Web Components are framework-agnostic and can be used across many different platforms, there are some specific details you need to consider when using them inside a React application. React and Web Components operate differently in certain areas, like attributes, state management, and event handling. In this section, we'll walk through how to integrate Web Components seamlessly into your React apps.
How Web Component Registration Works
At the heart of Web Components is the Custom Elements API, which allows developers to define new HTML elements and register them with the browser. The browser then treats these custom elements as native HTML tags.
To register a Web Component, you use the customElements.define()
method, which associates a custom element name (like <my-button>
) with a class that defines its behavior.
For example, here's how we register a simple MyButton
Web Component:
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<button>Click Me!</button>
`;
}
}
customElements.define('my-button', MyButton);
Once the component is defined and registered, the browser recognizes <my-button>
as a valid HTML element, and you can use it in any HTML file, just like a standard <div>
or <button>
.
But React works a bit differently, so we need to know how these two systems interact.
Why Do You Still Need to Import Web Components in React?
When working with Web Components in React, there’s a key step that can be easy to overlook: you still need to import or include the file where the Web Component is defined. Even though Web Components are registered globally with customElements.define()
, the registration code itself must be executed before React can use the component.
In simpler terms: even if <my-button>
is a valid custom element, React has no idea what it is until the file containing the Web Component’s registration (customElements.define()
) is loaded. That’s why you need to import the file or include it via a <script>
tag.
For example:
import './MyButton.js'; // Import the Web Component
function App() {
return (
<div>
<h1>Using Web Components in React</h1>
<my-button></my-button> {/* React will know this element only if it's been registered */}
</div>
);
}
export default App;
Without the import, React would render <my-button>
as an unstyled, inert HTML tag, because the browser wouldn’t know how to handle it.
Using Web Components in React: Practical Example
Let’s take a look at a practical example of using a Web Component in a React application. We’ll use the MyButton Web Component that we’ve defined earlier.
Step 1: Define the Web Component (MyButton.js
)
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
background-color: #007BFF;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
}
</style>
<button>Click Me!</button>
`;
}
}
customElements.define('my-button', MyButton);
Step 2: Use the Web Component in a React App
import React from 'react';
import './MyButton.js'; // Import the Web Component definition
function App() {
return (
<div>
<h1>Using Web Components in React</h1>
<my-button></my-button> {/* Now React knows about <my-button> */}
</div>
);
}
export default App;
In this example:
The Web Component (
<my-button>
) is rendered inside a React component.The custom element is recognized because it has been registered and imported in
MyButton.js
.
Handling Web Component Attributes in React
In React, we typically pass data to components via props. However, Web Components don’t have the same prop system. Instead, they interact with the DOM through attributes (e.g., setAttribute()
) and properties.
Why React Doesn't Automatically Map Props to Web Component Attributes
React handles props differently than the DOM handles attributes. While native DOM elements (like <input>
) automatically map props to attributes, React doesn't do this for custom elements (like <my-button>
). This means that if you pass a prop to a Web Component, it won’t automatically appear as an attribute.
To pass values to Web Components, we need to explicitly set the attributes using refs or directly interacting with the DOM API.
Setting Attributes with React Refs
To set attributes on a Web Component from React, we can use React refs. A ref allows us to directly access the DOM node and call methods or set attributes on it.
Here’s an example where we dynamically set an attribute on the MyButton
Web Component:
import React, { useEffect, useRef } from 'react';
import './MyButton.js'; // Import the Web Component
function App() {
const buttonRef = useRef(null); // Create a ref to access the Web Component
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.setAttribute('label', 'Click Me!'); // Set the 'label' attribute
}
}, []);
return (
<div>
<h1>Passing Attributes to Web Components in React</h1>
<my-button ref={buttonRef}></my-button> {/* Use the ref to interact with the component */}
</div>
);
}
export default App;
In this example:
We use
useRef
to get a reference to the Web Component.Inside
useEffect()
, we usesetAttribute()
to set thelabel
attribute on the Web Component.
Example: Passing React State to Web Components
React state doesn’t directly map to Web Component attributes, but we can update the attributes of a Web Component whenever the state changes. Let’s see how we can pass React state to a Web Component by updating its attributes dynamically.
import React, { useState, useEffect, useRef } from 'react';
import './MyButton.js'; // Import the Web Component
function App() {
const [label, setLabel] = useState('Initial Label');
const buttonRef = useRef(null);
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.setAttribute('label', label); // Set the Web Component's label attribute
}
}, [label]); // Run useEffect whenever 'label' state changes
return (
<div>
<h1>Passing React State to Web Components</h1>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="Enter button label"
/>
<my-button ref={buttonRef}></my-button> {/* Ref used to interact with the Web Component */}
</div>
);
}
export default App;
Here’s what happens:
React state (
label
) is updated when the user types in the input.Using the ref, we update the Web Component’s
label
attribute dynamically each time the state changes.
Handling Events Between Web Components and React
Web Components can dispatch custom events, just like native DOM elements emit events such as click
or change
. To capture these custom events in React, we need to add event listeners directly to the Web Component using refs.
React’s synthetic event system does not automatically capture custom events emitted from Web Components, so we need to rely on the standard addEventListener()
method.
Listening to Custom Events in React Using Refs
Let’s modify our Web Component to emit a custom event when the button is clicked:
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<button>Click Me!</button>
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
const event = new CustomEvent('buttonClicked', {
detail: { message: 'Button was clicked!' },
bubbles: true,
composed: true
});
this.dispatchEvent(event);
});
}
}
customElements.define('my-button', MyButton);
This Web Component emits a buttonClicked
event with a custom message when the button is clicked.
Now, in the React app, we can capture that custom event:
import React, { useEffect, useRef } from 'react';
import './MyButton.js'; // Import the Web Component
function App() {
const buttonRef = useRef(null);
useEffect(() => {
if (buttonRef.current) {
// Listen for the custom event 'buttonClicked'
const handleClick = (event) => {
alert(event.detail.message); // Display the message from the event's detail
};
buttonRef.current.addEventListener('buttonClicked', handleClick);
// Clean up the event listener when the component unmounts
return () => {
buttonRef.current.removeEventListener('buttonClicked', handleClick);
};
}
}, []);
return (
<div>
<h1>Handling Custom Events from Web Components in React</h1>
<my-button ref={buttonRef}></my-button> {/* Capture the custom event using ref */}
</div>
);
}
export default App;
Here’s what’s happening:
We attach an event listener for the custom event
buttonClicked
usingaddEventListener
.When the Web Component emits the event, we handle it by showing an alert with the event’s custom message (
event.detail.message
).We also clean up the event listener by removing it in the return function of the
useEffect
hook to prevent memory leaks.
Example: Two-Way Data Binding Between React and Web Components
We’ve seen how React can pass data to Web Components using attributes and how Web Components can send data back to React using custom events. To create two-way data binding between React and Web Components, we can combine these approaches.
Here’s an example where:
React passes data (a label) to the Web Component.
The Web Component sends a custom event back to React when its internal state changes (e.g., when the button is clicked).
Step 1: Modify the Web Component to Handle Two-Way Data Binding
Let’s extend our MyButton
Web Component to handle dynamic content and emit a custom event when clicked:
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
background-color: #007BFF;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
}
</style>
<button><slot></slot></button> <!-- Slot to allow dynamic content -->
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
const event = new CustomEvent('buttonClicked', {
detail: { message: 'Button was clicked!' },
bubbles: true,
composed: true
});
this.dispatchEvent(event); // Emit a custom event when the button is clicked
});
}
}
customElements.define('my-button', MyButton);
Step 2: Two-Way Data Binding in React
Now let’s create the React side, where we’ll:
Pass a label from React to the Web Component.
Listen for a custom event from the Web Component and update React state based on that event.
import React, { useState, useEffect, useRef } from 'react';
import './MyButton.js'; // Import the Web Component
function App() {
const [buttonLabel, setButtonLabel] = useState('Click Me!');
const [message, setMessage] = useState('');
const buttonRef = useRef(null);
useEffect(() => {
if (buttonRef.current) {
// Set the initial label of the button via the 'slot' content
buttonRef.current.innerHTML = buttonLabel;
// Listen for the custom 'buttonClicked' event
const handleClick = (event) => {
setMessage(event.detail.message); // Update React state when event is received
};
buttonRef.current.addEventListener('buttonClicked', handleClick);
// Cleanup the event listener on component unmount
return () => {
buttonRef.current.removeEventListener('buttonClicked', handleClick);
};
}
}, [buttonLabel]); // Update button label when state changes
return (
<div>
<h1>Two-Way Data Binding Between React and Web Components</h1>
<input
type="text"
value={buttonLabel}
onChange={(e) => setButtonLabel(e.target.value)} // Update button label based on input
placeholder="Enter button label"
/>
<my-button ref={buttonRef}></my-button> {/* Dynamically set the button label */}
{message && <p>{message}</p>} {/* Display the message from the Web Component */}
</div>
);
}
export default App;
What’s happening here:
Passing Data to the Web Component: We update the
innerHTML
of the Web Component (using a slot) to reflect the current state of thebuttonLabel
in React. This way, when the user types in the input, the Web Component’s label updates in real time.Receiving Data from the Web Component: When the button inside the Web Component is clicked, it emits a
buttonClicked
custom event. We listen for this event using refs and update the React state (message
), displaying the message on the screen.
This approach creates a two-way binding where:
React passes data to the Web Component (the button label).
The Web Component sends data back to React (via the custom event when the button is clicked).
Conclusion for Part 1
Web Components have revolutionized how we think about building and sharing UI components across multiple frameworks. By offering encapsulation, reusability, and framework-agnostic capabilities, they empower developers to create more flexible, maintainable systems. In this part of the blog, we laid the groundwork for understanding how Web Components can be integrated into React applications and shared across different tech stacks, providing practical examples and clear insights.
In Part 2, we’ll take things to the next level by diving into advanced usage patterns for Web Components in large-scale applications.