JavaScript Shadow DOM

The Shadow DOM is a key feature of Web Components, enabling developers to create encapsulated DOM trees and style scopes. This guide is designed to provide a deep understanding of the Shadow DOM, along with practical code examples to demonstrate its use.

What is Shadow DOM?

Shadow DOM allows developers to encapsulate a part of the DOM and its styles, isolating it from the rest of the document. This ensures that styles and scripts within the Shadow DOM do not clash with those in the main document.

<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

In this example, there are two elements with the same class name shadow-box. The first element is styled by the main document's CSS, while the second element is styled by the Shadow DOM's CSS. As you can see, the styles defined in the Shadow DOM do not affect the elements in the main document and vice versa. This demonstrates the encapsulation provided by the Shadow DOM, allowing you to create isolated and reusable components without worrying about style conflicts.

Creating a Shadow Root

To create a shadow root, use the attachShadow method on an element. The shadow root can be either open or closed. An open shadow root is accessible from JavaScript outside the shadow tree, while a closed shadow root is not.

Open Shadow Root

An open shadow root allows access and manipulation from external JavaScript. In the example below, we manipulate the text content inside the shadow root after the shadow root is created.

<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

In this example, a button is provided to change the content of the shadow DOM. Since the shadow root is open, we can access and manipulate its content from the main document.

Closed Shadow Root

A closed shadow root restricts access from external scripts, providing better encapsulation. In the example below, we try to manipulate the text content inside the shadow root after the shadow root is created, but it is not possible since it is closed.

<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // This will not work as intended because the shadow root is closed
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

In this example, an attempt to access and manipulate the shadow DOM content fails because the shadow root is closed. This demonstrates how closed shadow roots provide better encapsulation by restricting access.

Styling within Shadow DOM

When implementing JavaScript Shadow DOM, ensure proper encapsulation to prevent unintended styling or scripting conflicts.

Styles defined within a shadow root do not affect elements outside it, and vice versa. This encapsulation is beneficial for creating reusable components.

<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

In this example, there are two elements with the class name styled-box. The first element is styled by the main document's CSS, while the second element is styled by the Shadow DOM's CSS. The styles defined in the Shadow DOM do not affect the elements in the main document, and the styles defined in the main document do not affect the elements in the Shadow DOM. This demonstrates how Shadow DOM encapsulates styles, ensuring no conflicts between the component's styles and the global styles.

Slotting: Light DOM Content in Shadow DOM

Slots allow developers to pass light DOM (regular DOM) content into a shadow DOM, making the shadow DOM more flexible and reusable.

<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

In this example, the <slot> element is used to pass content from the light DOM into the shadow DOM. The slot attribute on the span element matches the name attribute of the slot element in the shadow DOM, allowing the span content to be projected into the shadow DOM.

JavaScript Interaction with Shadow DOM

Interacting with the Shadow DOM via JavaScript requires understanding the encapsulation boundaries. Direct manipulation within the shadow root is straightforward, but external interaction requires careful handling.

Accessing Shadow DOM Elements

To access elements within a shadow DOM, use the shadowRoot property.

<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.getElementById('shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

In this example, the <slot> element is used to pass content from the light DOM into the shadow DOM. The slot attribute on the span element matches the name attribute of the slot element in the shadow DOM, allowing the span content to be projected into the shadow DOM.

Shadow DOM Practical Examples

Creating a Reusable Web Component

Creating a reusable web component using Shadow DOM involves defining a custom element and attaching a shadow root to it.

<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

In this example, a custom element <custom-card> is created with a shadow DOM. The shadow DOM encapsulates the styles and structure of the component, making it reusable without worrying about style conflicts with the main document.

Integrating with Frameworks

Shadow DOM can be used seamlessly with modern JavaScript frameworks like React, Angular, and Vue.

React Example

In React, you can create a custom element and integrate it with Shadow DOM as follows:

<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useEffect } = React;

    const CustomCard = ({ title, children }) => {
      const cardRef = useRef(null);

      useEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${children}</div>
            </div>
          `;
        }
      }, [title, children]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World">
        This is content inside the shadow DOM.
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

In this example, a custom element <custom-card> is defined and used within a React component. The Shadow DOM ensures that the styles and structure of the custom element are encapsulated, providing seamless integration with React.

Conclusion

Mastering Shadow DOM is essential for modern web development, providing powerful encapsulation and reusability. By understanding and implementing the concepts and examples provided, you can create robust, isolated components that enhance the maintainability and scalability of your web applications.

This comprehensive guide should serve as a solid foundation for exploring and utilizing Shadow DOM in your projects. Whether you are building simple widgets or complex applications, the Shadow DOM offers the encapsulation and flexibility needed to ensure your components remain isolated and manageable.

Practice Your Knowledge

Which method is used to create a shadow root in JavaScript?

Quiz Time: Test Your Skills!

Ready to challenge what you've learned? Dive into our interactive quizzes for a deeper understanding and a fun way to reinforce your knowledge.

Do you find this helpful?