JavaScript - JavaScript Web Components and Shadow DOM
Introduction
Web Components are a set of modern web platform technologies that allow developers to create reusable, custom HTML elements. Instead of repeatedly writing the same HTML, CSS, and JavaScript for a user interface component, developers can create a custom element once and reuse it throughout a website or application.
For example, instead of creating the same card structure repeatedly:
<div class="user-card">
<h2>John</h2>
<p>Web Developer</p>
</div>
you can create your own HTML element:
<user-card></user-card>
The behavior and appearance of <user-card> can then be defined using JavaScript.
Web Components are based on browser-native standards and do not require a particular JavaScript framework. They can be used with plain JavaScript as well as alongside frameworks and libraries.
The main technologies associated with Web Components are:
-
Custom Elements
-
Shadow DOM
-
HTML Templates
-
HTML Slots
1. Custom Elements
Custom Elements allow developers to define their own HTML elements.
A custom element generally has a name containing a hyphen. Examples include:
<user-profile></user-profile>
<product-card></product-card>
<login-form></login-form>
JavaScript's customElements.define() method is used to register a custom element.
Basic Example
class UserProfile extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<h2>John Doe</h2>
<p>Web Developer</p>
`;
}
}
customElements.define("user-profile", UserProfile);
The element can then be used in HTML:
<user-profile></user-profile>
The browser recognizes the custom element and creates an instance of the UserProfile class.
2. Why Use Custom Elements?
Custom Elements are useful when the same user-interface component needs to appear in multiple locations.
For example, suppose an application contains several product cards. Without a reusable component, the developer may have to repeat the same HTML structure many times.
With a custom element:
<product-card></product-card>
<product-card></product-card>
<product-card></product-card>
the same component structure can be reused.
This provides several advantages:
-
Reusability
-
Better organization
-
Separation of component logic
-
Easier maintenance
-
Encapsulation
-
Consistent user-interface behavior
3. Shadow DOM
The Shadow DOM is a browser feature that allows a component to have its own private DOM tree.
Normally, HTML elements are part of the document's main DOM.
For example:
<div>
<h2>Hello</h2>
</div>
The <h2> element belongs to the document's regular DOM.
With Shadow DOM, a component can create an isolated DOM structure inside itself.
Consider:
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<h2>John Doe</h2>
<p>Web Developer</p>
`;
}
}
customElements.define("user-card", UserCard);
The important line is:
this.attachShadow({ mode: "open" });
This creates a Shadow Root for the component.
The component's internal elements are then placed inside that Shadow Root.
4. Shadow DOM Encapsulation
One of the most important advantages of Shadow DOM is encapsulation.
Suppose the main page contains:
h2 {
color: red;
}
Normally, this CSS can affect <h2> elements throughout the document.
However, a component can define its own internal styles:
shadow.innerHTML = `
<style>
h2 {
color: blue;
}
</style>
<h2>John Doe</h2>
`;
The component's internal <h2> is styled independently.
This helps prevent unwanted conflicts between the CSS of the main application and the CSS of a reusable component.
5. Complete Web Component Example
Here is a simple custom component:
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
.card {
border: 1px solid #ccc;
padding: 20px;
width: 250px;
border-radius: 8px;
}
h2 {
margin: 0;
}
p {
color: gray;
}
</style>
<div class="card">
<h2>John Doe</h2>
<p>Web Developer</p>
</div>
`;
}
}
customElements.define("user-card", UserCard);
HTML:
<user-card></user-card>
The browser renders the component using its own internal HTML and CSS.
6. Shadow DOM Modes
When creating a Shadow Root, two commonly discussed modes are:
this.attachShadow({ mode: "open" });
and:
this.attachShadow({ mode: "closed" });
Open Shadow DOM
With an open Shadow Root, JavaScript outside the component can access the Shadow Root.
Example:
const card = document.querySelector("user-card");
console.log(card.shadowRoot);
This returns the component's Shadow Root.
Therefore, developers can inspect and interact with the component's internal DOM through shadowRoot.
Closed Shadow DOM
With a closed Shadow Root:
this.attachShadow({ mode: "closed" });
the Shadow Root is not exposed through the element's shadowRoot property.
const card = document.querySelector("user-card");
console.log(card.shadowRoot);
This returns:
null
Closed mode provides stronger encapsulation from outside code, although it should not be treated as a security boundary.
7. HTML Templates
The <template> element allows developers to define HTML that is not immediately rendered when the page loads.
Example:
<template id="userTemplate">
<div class="user">
<h2>John Doe</h2>
<p>Developer</p>
</div>
</template>
The contents of the template are stored until JavaScript uses them.
JavaScript can access the template:
const template = document.getElementById("userTemplate");
const copy = template.content.cloneNode(true);
document.body.appendChild(copy);
Templates are particularly useful for Web Components because they allow component structures to be defined separately from JavaScript logic.
8. Using Templates with Shadow DOM
A Web Component can use both <template> and Shadow DOM.
Example:
<template id="cardTemplate">
<style>
.card {
border: 1px solid black;
padding: 15px;
}
</style>
<div class="card">
<h2>User Profile</h2>
<p>Welcome to the website.</p>
</div>
</template>
JavaScript:
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const template =
document.getElementById("cardTemplate");
shadow.appendChild(
template.content.cloneNode(true)
);
}
}
customElements.define("user-card", UserCard);
HTML:
<user-card></user-card>
This approach separates the component's HTML structure from its JavaScript implementation.
9. HTML Slots
Slots allow a Web Component to accept content from the HTML that uses the component.
For example:
<user-card>
<h2>John Doe</h2>
<p>Software Developer</p>
</user-card>
The component can provide a location where this external content should appear.
Example:
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<div class="card">
<slot></slot>
</div>
`;
}
}
customElements.define("user-card", UserCard);
The <slot> acts as a placeholder for the content supplied between the opening and closing <user-card> tags.
10. Named Slots
A component can contain multiple slots.
For example:
<user-card>
<span slot="name">John Doe</span>
<span slot="role">Software Developer</span>
</user-card>
The component can define:
shadow.innerHTML = `
<div>
<h2>
<slot name="name"></slot>
</h2>
<p>
<slot name="role"></slot>
</p>
</div>
`;
Here:
slot="name"
is connected to:
<slot name="name"></slot>
Similarly:
slot="role"
is connected to:
<slot name="role"></slot>
Named slots make components more flexible because users can provide different content for different areas.
11. Custom Element Lifecycle
Custom Elements provide lifecycle callbacks that allow developers to execute code when certain events occur.
Important lifecycle methods include:
connectedCallback()
disconnectedCallback()
attributeChangedCallback()
adoptedCallback()
connectedCallback()
This method runs when the element is inserted into the document.
Example:
class UserCard extends HTMLElement {
connectedCallback() {
console.log("User card added to the page");
}
}
customElements.define("user-card", UserCard);
disconnectedCallback()
This runs when the element is removed from the document.
disconnectedCallback() {
console.log("User card removed");
}
It can be useful for cleaning up event listeners, timers, or other resources.
12. Observed Attributes
A Web Component can respond to changes in specific HTML attributes.
For example:
<user-card name="John"></user-card>
JavaScript:
class UserCard extends HTMLElement {
static get observedAttributes() {
return ["name"];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name);
console.log(oldValue);
console.log(newValue);
}
}
customElements.define("user-card", UserCard);
If the attribute changes:
document
.querySelector("user-card")
.setAttribute("name", "David");
the attributeChangedCallback() method is called.
13. Passing Data to Custom Elements
Attributes can be used to customize a component.
HTML:
<user-card
name="John Doe"
role="Developer">
</user-card>
JavaScript:
class UserCard extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name");
const role = this.getAttribute("role");
this.innerHTML = `
<h2>${name}</h2>
<p>${role}</p>
`;
}
}
customElements.define("user-card", UserCard);
The same component can now display different users.
<user-card name="John" role="Developer"></user-card>
<user-card name="Sarah" role="Designer"></user-card>
This demonstrates the reusable nature of Web Components.
14. Shadow DOM and Regular DOM
It is important to understand the difference between the regular DOM and Shadow DOM.
| Feature | Regular DOM | Shadow DOM |
|---|---|---|
| Part of main document tree | Yes | Separate shadow tree |
| CSS isolation | Limited | Stronger encapsulation |
| Reusable component structure | Possible | Well suited |
| Internal structure exposed normally | Yes | Encapsulated |
| Custom elements required | No | No |
| Component-oriented design | Possible | Very suitable |
Shadow DOM does not replace the regular DOM. Instead, it provides an additional mechanism for creating encapsulated component internals.
15. Events in Web Components
Web Components can also respond to user actions.
Example:
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<button>Click Me</button>
`;
shadow
.querySelector("button")
.addEventListener("click", () => {
console.log("Button clicked");
});
}
}
customElements.define("my-button", MyButton);
The button exists inside the Shadow DOM, but the component itself can communicate with the outside document using events.
For example:
this.dispatchEvent(
new CustomEvent("user-selected", {
detail: {
name: "John"
},
bubbles: true
})
);
An outside element can listen for this event:
document.addEventListener("user-selected", event => {
console.log(event.detail.name);
});
This allows a Web Component to communicate with the application that contains it.
16. Benefits of Web Components
Web Components provide several important benefits.
Reusability
A component can be created once and used many times.
<product-card></product-card>
Encapsulation
Shadow DOM helps keep internal HTML and CSS separate from the surrounding application.
Framework Independence
Web Components are based on web standards rather than being tied to a particular JavaScript framework.
Maintainability
A complex interface can be divided into smaller independent components.
Consistency
The same component can provide consistent behavior and appearance wherever it is used.
Portability
A properly designed Web Component can potentially be reused across different applications and projects.
17. Limitations and Considerations
Web Components are powerful, but they are not always the best solution for every project.
Developers should consider:
-
Component design can become complex for large applications.
-
Shadow DOM introduces additional concepts that developers need to understand.
-
State management is not provided automatically.
-
Developers must design communication between components carefully.
-
Accessibility still needs to be handled properly.
-
Some framework-specific features may require additional integration work.
Web Components provide the building blocks, but application architecture is still the developer's responsibility.
18. Practical Example
Consider a website that displays employee information.
HTML:
<employee-card
name="Anita"
department="Engineering">
</employee-card>
<employee-card
name="Rahul"
department="Marketing">
</employee-card>
JavaScript:
class EmployeeCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
.card {
padding: 20px;
border: 1px solid #ccc;
margin: 10px;
}
h2 {
margin-bottom: 5px;
}
</style>
<div class="card">
<h2 class="name"></h2>
<p class="department"></p>
</div>
`;
}
connectedCallback() {
this.shadowRoot.querySelector(".name")
.textContent = this.getAttribute("name");
this.shadowRoot.querySelector(".department")
.textContent = this.getAttribute("department");
}
}
customElements.define("employee-card", EmployeeCard);
The same component is reused for multiple employees while the internal structure and styling remain encapsulated.
19. Web Components in Modern Web Development
Web Components are particularly useful when an application needs reusable UI elements that can work across different environments.
For example, a company could create:
<company-header></company-header>
<company-button></company-button>
<employee-card></employee-card>
<product-card></product-card>
These components could then be used across multiple websites or applications.
They are especially useful for design systems, reusable UI libraries, embedded widgets, and applications that need components independent of a particular framework.
20. Key Concepts to Remember
The most important concepts in JavaScript Web Components and Shadow DOM are:
Custom Elements:
Allow developers to create their own HTML elements.
Shadow DOM:
Provides an encapsulated DOM tree for a component.
Shadow Root:
The root of the component's Shadow DOM.
HTML Templates:
Allow reusable HTML structures to be defined without immediately rendering them.
Slots:
Allow external HTML content to be inserted into designated locations inside a Web Component.
Lifecycle Callbacks:
Allow components to react when they are added, removed, or modified.
Attributes:
Allow data and configuration to be passed to custom elements.
Custom Events:
Allow Web Components to communicate with the surrounding application.
Conclusion
JavaScript Web Components and Shadow DOM provide a standards-based way to build reusable, modular, and encapsulated user-interface components. Custom Elements define new HTML elements, Shadow DOM isolates their internal structure and styles, Templates provide reusable markup, and Slots allow components to accept flexible content.
Understanding these technologies is valuable for advanced JavaScript development because they demonstrate how modern browsers can support component-based application design without requiring a specific framework.