DEV Community

Cover image for Tips from Open source: Use shadow DOM to avoid potential CSS breaks
Ramu Narasinga
Ramu Narasinga

Posted on • Edited on

Tips from Open source: Use shadow DOM to avoid potential CSS breaks

This tip is picked from Next.js source code. In this article, you will learn the following:

  1. What is a shadow DOM?
  2. Key Features of Shadow DOM
  3. Modes of Shadow DOM
  4. How Next.js used shadow DOM in an announcer related code
  5. Shadow DOM usage in Open source

What is a shadow DOM?

The Shadow DOM is a web standard that allows developers to encapsulate a piece of the DOM and CSS in a way that isolates it from the rest of the document. This encapsulation provides several benefits, such as avoiding CSS conflicts and making reusable web components more manageable. You can read more about shadow DOM in MDN docs.

Key Features of Shadow DOM

Encapsulation

Shadow DOM allows you to create a subtree of DOM elements that is isolated from the main document’s DOM. This means that styles and scripts within the Shadow DOM do not affect the rest of the document, and vice versa.

Shadow Root:

The entry point of the Shadow DOM is called the Shadow Root. You can attach a Shadow Root to an element using JavaScript, and this root acts as a boundary for the encapsulated DOM.

Isolation:

Styles defined inside the Shadow DOM do not leak out to the main document, and styles from the main document do not affect the Shadow DOM. This isolation helps in preventing unintended side effects and conflicts.

Example

Here’s a simple example demonstrating how to use Shadow DOM:

<!DOCTYPE html>
<html>
<head>
  <style>
    /\* This style will not affect the Shadow DOM \*/
    div {
      color: red;
    }
  </style>
</head>
<body>

<div id="host">Hello, world!</div>
<div id="example">This example is outside the DOM</div>

<script>
  // Select the host element
  const host = document.getElementById('host');

  // Create a shadow root and attach it to the host
  const shadowRoot = host.attachShadow({ mode: 'open' });

  // Add some content to the shadow root
  shadowRoot.innerHTML = \`
    <style>
      /\* This style is scoped to the shadow DOM \*/
      p {
        color: green;
      }
    </style>
    <p>This is inside the Shadow DOM</p>
  \`;
</script>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

In this example:

  1. The element with the ID host is the host element for the Shadow DOM.
  2. A Shadow Root is attached to this host element using host.attachShadow({ mode: ‘open’ }).
  3. Inside the Shadow Root, a paragraph (

    ) element is styled with green text. This style is isolated within the Shadow DOM and does not affect the rest of the document.

  4. The red text color defined in the main document does not affect the paragraph inside the Shadow DOM.
  5. Modes of Shadow DOM

    There are two modes for creating a Shadow Root:

    1. Open: The shadow DOM can be accessed and manipulated from JavaScript outside the Shadow Root.
    2. Closed: The shadow DOM is encapsulated more strictly, and it cannot be accessed from JavaScript outside the Shadow Root.
    const existingAnnouncer = document.getElementsByName(ANNOUNCER\_TYPE)\[0\]
      if (existingAnnouncer?.shadowRoot?.childNodes\[0\]) {
        return existingAnnouncer.shadowRoot.childNodes\[0\] as HTMLElement
    }
    

    In the above code snippet from Next.js source code, existingAnnouncer?.shadowRoot would return null when the mode is closed.

    How Next.js used shadow DOM in an announcer related code

    You will find that app-router-announcer.tsx has the following code:

    export function AppRouterAnnouncer({ tree }: { tree: FlightRouterState }) {
      const \[portalNode, setPortalNode\] = useState<HTMLElement | null>(null)
    
      useEffect(() => {
        const announcer = getAnnouncerNode()
        setPortalNode(announcer)
        return () => {
          const container = document.getElementsByTagName(ANNOUNCER\_TYPE)\[0\]
          if (container?.isConnected) {
            document.body.removeChild(container)
          }
        }
      }, \[\])
    

    getAnnouncerNode() is as shown below in the same file:

    Mode used is “open” meaning this shadow can be accessed by Browser’s Javascript, hence the reason why you see existingAnnouncer?.shadowRoot?.childNodes[0] in the if block.

    createPortal is used to insert this shadowDom in the document.body.

    Shadow DOM usage in Open source:

    Search results on Github: https://github.com/search?q=attachShadow%28%7B+mode%3A+%27open%27+%7D%29&type=code

    Meteor.js is found to use shadowDOM in dev error overlay — https://github.com/meteor/meteor/blob/02aa69c0c2d2c3ea805ba8bcf0dbaa565ca995b3/packages/dev-error-overlay/client.js#L57

    And few more examples below:

    1. https://github.com/TanStack/query/blob/1d60d44ad851472d37aaa3eef22ada07e5245989/examples/react/shadow-dom/src/main.tsx#L12
    2. https://github.com/cypress-io/cypress/blob/5569da09e49c4de741a8a6e8c63b865e4710acf7/packages/app/src/runner/dom.ts#L39

    Conclusion:

    I have not seen shadowRoot found in app-router-announcer before and this led me to find out about shadowDOM and how shadowRoot is related to this concept. shadow DOM is found to be used in banners/announcements(next.js) or error overlay (meteor.js) to avoid css breaks in other parts of the app because shadowDOM prevents the css override and can have its own scripts and styles that can be applied to the shadow tree.

    You could apply this similar pattern to your npm packages to not let your package’s CSS affect the other parts of the app. This is one of many ways to prevent CSS overrides.

    Feel free to reach out to me at ramu.narasinga@gmail.com if you have any questions about this article.

    Get free courses inspired by the best practices used in open source.

    About me:

    Website: https://ramunarasinga.com/

    Linkedin: https://www.linkedin.com/in/ramu-narasinga-189361128/

    Github: https://github.com/Ramu-Narasinga

    Email: ramu.narasinga@gmail.com

    Learn the best practices used in open source.

    References:

    1. https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router-announcer.tsx#L23
    2. https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L660
    3. https://stackoverflow.com/questions/34119639/what-is-shadow-root
    4. https://www.webcomponents.org/community/articles/introduction-to-shadow-dom
    5. https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM
    6. https://react.dev/reference/react-dom/createPortal

Top comments (0)