Web Components
Learn JavaScript web components from scratch: custom elements, Shadow DOM, HTML templates, slots, and lifecycle callbacks, with examples.
Web components let you create your own reusable, encapsulated HTML elements — custom tags like <user-card> or <star-rating> that carry their own markup, styles, and behavior. Because they are built on native browser APIs, they work in any framework (or no framework at all) and keep their internals isolated from the rest of the page. This guide covers the three building blocks of web components, walks through building one from scratch, and ends with lifecycle hooks and best practices.
When would I use web components?
Reach for web components when you want a self-contained UI widget that:
- Works everywhere — the same
<my-widget>drops into a React, Vue, plain-HTML, or server-rendered page with no build step. - Stays isolated — its styles cannot leak out and the page's styles cannot leak in, thanks to the Shadow DOM.
- Ships as a single file — markup, style, and logic live together, so the component is easy to distribute and reuse.
Design systems, embeddable widgets, and shared component libraries are the most common use cases.
Introduction to Web Components
Web components are a set of web platform APIs that let you create new custom, reusable, encapsulated HTML tags. Three technologies work together:
- Custom Elements — define new HTML tags and their behavior.
- Shadow DOM — encapsulate a component's markup and styles so they are scoped to the element.
- HTML Templates — declare reusable chunks of inert markup that you stamp out at runtime.
Custom Elements
Custom elements define new HTML tags and their behavior. They build on JavaScript classes: a custom element is a class that extends HTMLElement. Once registered, the tag can be used like any standard HTML element.
<div>
<script>
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<p>Hello, World!</p>';
}
}
customElements.define('my-element', MyElement);
</script>
<my-element></my-element>
</div>This code snippet demonstrates how to create a custom HTML element called my-element. The MyElement class extends HTMLElement, attaches a shadow DOM to encapsulate its content, and inserts a simple paragraph with the text "Hello, World!".
Shadow DOM
Shadow DOM provides encapsulation for your custom elements. It lets you attach a separate, hidden DOM tree (the shadow tree) to an element, keeping its styles and markup scoped to the element itself. For a deeper dive, see the dedicated chapter on the JavaScript Shadow DOM.
<div>
<script>
class ShadowElement extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
p { color: blue; }
</style>
<p>Shadow DOM content</p>
`;
}
}
customElements.define('shadow-element', ShadowElement);
</script>
<shadow-element></shadow-element>
</div>In this example, the ShadowElement class extends HTMLElement and attaches a shadow root to the element. The mode: 'open' parameter specifies that the shadow DOM is accessible via JavaScript. When mode is set to 'open', the shadow root can be accessed using the element.shadowRoot property. If mode is set to 'closed', the shadow root is not accessible from the outside, enhancing encapsulation and preventing external scripts from manipulating the shadow DOM directly.
Within the shadow DOM, we define some styles and HTML content. The styles (in this case, a paragraph styled with blue text) are scoped to the shadow DOM, ensuring they do not affect any other elements on the page. This encapsulation is beneficial for creating reusable components with consistent styling and behavior, regardless of the context in which they are used.
Use local styles within the Shadow DOM to prevent style leakage and ensure component styles are scoped.
For more on scoping CSS to a shadow tree — including :host and CSS custom properties that can cross the boundary — see Shadow DOM styling.
HTML Templates
HTML templates let you define reusable chunks of HTML that are inert — they are parsed but not rendered, and their scripts do not run — until you clone and insert them at runtime. The <template> element is the standard way to do this.
<div>
<template id="my-template">
<style>
p { color: red; }
</style>
<p>This is a template content</p>
</template>
<script>
const template = document.getElementById('my-template').content;
document.body.appendChild(template.cloneNode(true));
</script>
</div>This example shows how to use HTML templates. The <template> tag contains HTML and styles that are not rendered immediately. The script clones the template's content and appends it to the document body, making it visible.
Building Your First Web Component
Let's walk through the process of creating a fully functional web component that can be used across different projects.
Step 1: Define the Component
Create a new class that extends HTMLElement.
<div>
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = 'Hello, World!';
}
}
customElements.define('my-component', MyComponent);
</script>
<my-component></my-component>
</div>This snippet sets the foundation for a web component called my-component. It defines a class that extends HTMLElement and attaches a shadow DOM, preparing it for further customization.
Step 2: Add Styles and Templates
Use the Shadow DOM to encapsulate styles and templates.
<div>
<script>
class MyStyledComponent extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
p { font-size: 20px; color: green; }
</style>
<p>This is my styled component!</p>
`;
}
}
customElements.define('my-styled-component', MyStyledComponent);
</script>
<my-styled-component></my-styled-component>
</div>This snippet enhances my-styled-component by adding scoped styles within the shadow DOM. The paragraph within the component is styled with green color and increased font size.
Step 3: Add Interactivity
Add JavaScript to make your component interactive.
<div>
<script>
class InteractiveComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
p { font-size: 20px; }
</style>
<p>This is interactive!</p>
<button>Click me</button>
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.shadowRoot.querySelector('p').textContent = 'You clicked!';
});
}
}
customElements.define('interactive-component', InteractiveComponent);
</script>
<interactive-component></interactive-component>
</div>This example shows how to make a web component interactive. The InteractiveComponent includes a button that, when clicked, changes the text content of a paragraph within the component.
Advanced Web Component Techniques
Lifecycle Callbacks
Custom elements have lifecycle callbacks that allow you to run code during specific phases of an element’s life.
<div>
<script>
class LifecycleComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = '<p>Element added to page.</p>';
}
disconnectedCallback() {
console.log('Element removed from page.');
}
}
customElements.define('lifecycle-component', LifecycleComponent);
</script>
<lifecycle-component></lifecycle-component>
</div>This snippet demonstrates the use of lifecycle callbacks in web components. The LifecycleComponent updates its content when added to the page and logs a message when removed.
Attribute Change Handling
React to changes in an element’s attributes by defining the observedAttributes array and implementing the attributeChangedCallback method.
<div>
<script>
class AttributeComponent extends HTMLElement {
static get observedAttributes() {
return ['data-text'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<p></p>';
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data-text') {
this.shadowRoot.querySelector('p').textContent = newValue;
}
}
}
customElements.define('attribute-component', AttributeComponent);
</script>
<attribute-component data-text="Initial text"></attribute-component>
<script>
const element = document.querySelector('attribute-component');
setTimeout(() => {
element.setAttribute('data-text', 'Updated text');
}, 2000);
</script>
</div>This example shows how to handle attribute changes in a web component. The AttributeComponent updates its internal content based on changes to the data-text attribute. Note that attributeChangedCallback only fires for attributes listed in observedAttributes — without that list, attribute changes are ignored.
The four custom-element lifecycle callbacks are:
| Callback | When it runs |
|---|---|
constructor() | When the element is created or upgraded. Set up initial state here, but avoid touching attributes or child DOM. |
connectedCallback() | Each time the element is inserted into the document. Best place to render and add event listeners. |
disconnectedCallback() | Each time the element is removed from the document. Clean up listeners and timers here. |
attributeChangedCallback(name, oldVal, newVal) | When an observed attribute changes. Requires a static observedAttributes getter. |
Distributing Children with Slots
A <slot> lets a component render the markup a user places between its tags, so consumers can pass in their own content. This is the basis of composition with web components — see Shadow DOM, slots and composition for the full picture.
<div>
<script>
class CardBox extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.box { border: 1px solid #ccc; padding: 8px; }
</style>
<div class="box"><slot>Default content</slot></div>
`;
}
}
customElements.define('card-box', CardBox);
</script>
<card-box>Passed-in content</card-box>
<card-box></card-box>
</div>The first <card-box> renders "Passed-in content" inside the styled box; the second falls back to the slot's default "Default content".
Best Practices for Web Components
Use Shadow DOM Wisely
Encapsulate styles and scripts within the Shadow DOM to prevent conflicts with other elements on the page.
Follow Naming Conventions
Custom elements must contain a hyphen in their name (for example, my-card, not mycard) to avoid conflicts with current and future standard HTML elements. Registering a name that lacks a hyphen throws an error.
Keep Components Modular
Create small, reusable components to keep your codebase maintainable and scalable.
Avoid Global Styles
Use local styles within the Shadow DOM to prevent style leakage and ensure component styles are scoped.
Documentation and Testing
Document your components well and write tests to ensure they work as expected across different browsers and use cases.
Conclusion
Web components are a versatile and powerful feature in modern web development, enabling the creation of reusable, encapsulated custom elements. By understanding and utilizing Custom Elements, Shadow DOM, and HTML Templates, you can create modular, maintainable, and scalable web applications. This guide has provided you with the knowledge and examples needed to get started with web components, ensuring you can effectively implement them in your projects.
To go further, explore the JavaScript Shadow DOM, how Shadow DOM and events interact, and the <template> element in depth.