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:

  1. Custom Elements

  2. Shadow DOM

  3. HTML Templates

  4. 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.