Winning the war of CSS conflicts through the Shadow DOM

Andre Khong
Rate Engineering
Published in
9 min readAug 21, 2018

Do you remember the time when you worked on a web application project and discovered that your implementation for a new feature was messed up because an existing CSS style selector used the !important declaration which overrode your styling rules?

Here at Rate, we encounter many instances of such issues on one of our products, RateX. RateX is a browser extension available on Chrome and Firefox which helps you to pay less when you shop online. Due to the nature of the product as a browser extension, when we inject DOM into a webpage to render our extension, there is a good chance that some of our stylings might be broken — simply because of conflicting style declarations.

If your development involves building on top of an existing web project — such as:

  • A browser extension (like ours)
  • Adding custom components to a Content Management System (CMS)
  • An isolated component in a huge web application

— and modifying the existing stylesheets is not an option, then this article may offer an alternative solution to your problems.

Fret not, for the war of CSS conflicts can be won — through the Shadow DOM.

Shadow DOM? What’s that?

For those of you who are wondering, Shadow DOM is a special node within the main document that serves as a boundary to segregate styles from the rest of the document. In contrast, the plain old DOM tree that we all are accustomed to (directly accessible through the document variable in JavaScript) is known as the Light DOM.

(Source: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM)

Basically, this means that stylesheets loaded into the Light DOM will only apply to elements within the Light DOM, while stylesheets loaded into the Shadow DOM only applies to elements within that particular Shadow DOM. This effectively encapsulates and isolates declared styles within its own boundary.

Within a Shadow DOM boundary, there exists a root node known as the Shadow Root. The Shadow Root must be hosted by a node, which typically exists in the Light DOM. The hosting element is thus known as the Shadow Host.

Confused? Don’t worry! Here’s a quick summary of the terms we went through.

Terminologies

  • Light DOM: Good ole’ regular DOM
  • Shadow DOM: DOM within the shadow boundary
  • Shadow Host: Light DOM node hosting the Shadow Root
  • Shadow Root: Root node of the Shadow DOM

Fun fact: Multiple Shadow DOMs can be hosted in the document, and can even be nested, but each node can only play host to one Shadow Root!

Now that we are clear on what’s what, let’s dive in a little deeper to see how it works.

Dwelving into the Shadows

Note: At the time of writing, only Google Chrome fully supports the Shadow DOM spec. Firefox by default does not support Shadow DOM, but the feature can be enabled. Check out which browsers are compatible here. As such, we highly recommend that you experiment with either Chrome or Firefox.

Before we can actually make use of the Shadow DOM, we need an element to play host to the shadow root. We can do this by creating a shadow root on a host node.

// Select your shadow host
const shadowHost = document.getElementById('my-shadow-host');
// Create the shadow root
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

Tip 1: Choose your shadow host wisely for some elements are unable to host the shadow root! Refer to the MDN docs for more information.

Tip 2: Avoid selecting a node with children to act as the shadow host. The hosting node will “disappear” when a shadow root is attached.

Creating nodes for the Shadow DOM

With the shadow root created, we can now create our own elements that are unaffected by the existing styles in the rest of the document.

There are a couple ways we can do this:

  • Vanilla Javascript
// Create a div element
const myDivElement = document.createElement('div');
myDivElement.setAttribute('id', 'my-shadow-div');
myDivElement.innerHTML = 'some content or html to render';
// Append div to shadow DOM
shadowRoot.appendChild(myDivElement);

This approach will work, but is definitely tedious and may get unwieldy when dealing with larger components. Styling will require creating a <style> element with style rules written in code and then appending it into the shadow root.

  • Using Templates and Slots
// Assuming you are able write into the web application DOM and
// have a <template> element created
const myTemplate = document.getElementById('my-template');
const myTemplateContent = myTemplate.content;
// Add template content to shadow DOM
shadowRoot.appendChild(myTemplateContent.cloneNode(true));

Refer to the MDN docs for a more complete guide on using templates and slots.

This approach is definitely preferable to using vanilla Javascript, but will require you (the developer) to have access to write into the webpage — a luxury not all of us may have.

A “real-world” use case for Shadow DOM

Okay, so now we know how to create elements within the Shadow DOM. But under what circumstances would we use it? Let’s take a look at an example that most of us should be able to relate to.

Scenario:
Assume we are working on an existing project which uses the Bootstrap CSS framework, and there’s a custom stylesheet written to override some of the default stylings shipped with Bootstrap.

Our task is to introduce a new component, a search bar, to the application.

Existing code sample:
Here’s a sample markup of the project we are dealing with (trivialized version)

Base sample code

How it looks like:

Base sample application

Requirements:
Let’s add a search functionality to the navigation bar.

Assuming we want to simply add an <input> element, we could modify the application as such:

Code changes to add a new input element

Our application now looks like this:

After adding an input element

Notice how the existing custom stylesheet rule(s) interfere with the styling on our new Search <input> element. Now the Search element has a height: 50px; style applied to it.

Potential Pitfall

Assuming we do not want any height styling to be applied to our Search element, we could simply assign an id and add a new CSS rule height: unset !important; that will apply to the element.

Sure, but do we really want to do this?

This will certainly be possible for a trivial application such as the example shown above. In reality, the existing application could be much more complex, and achieving the desired styling for the component/element may require even more specific styling definitions.

Moreover, if there are modifications to the existing stylesheet in the future, it could potentially lead to regression issues for this element. Managing such changes could definitely prove to be a nightmare for larger applications.

So.. how can we avoid this problem?

Shadow DOM to the rescue

A better approach would be to place our Search element within the Shadow DOM.

Modifications to the code:

Code changes to place Search element in Shadow DOM

How it looks like with Shadow DOM:

Our search component completely devoid of external styles

Now that our new search component is within the Shadow DOM, we can style it to our heart’s content without worrying about external stylesheets interfering with our defined styling rules.

Some helpful pointers to note when working with Shadow DOM

Event retargeting

If, for example, you are trying to detect a click event on some element within the Shadow DOM, be mindful of where you attach the event listener.

Doing:

document.addEventListener('click', function (event) { ... });

VS

shadowRoot.addEventListener('click', function (event) { ... });

will yield different results on the event.target element. If you attach the click event listener to document and click on an element within the Shadow DOM, the event is observed as originating from the Shadow Host rather than the actual element in the Shadow DOM that is being clicked.

Adding custom Fonts

If you want to add custom fonts to your elements within the Shadow DOM, load the font-face into the Light DOM and then apply the font styles to the Shadow DOM (loading @font-face directly into Shadow DOM will NOT work!).

Summary

We’ve come to the end of this quick introduction into the realm of the Shadow DOM. You should now have a better understanding of what Shadow DOM is all about and how it may be useful in solving certain development issues.

If you are keen to understand more about Shadow DOM, here are some useful resources that you should not miss:

ADDITIONAL: What if I’m using React?

If like us, you’re working with React and also need to utilize the Shadow DOM, here’s a really brief quickstart guide to help you get going.

Some Assumptions:

  • Your project is using a setup similar to Create React App (CRA) — minimally with webpack, babel, css-loader and style-loader.
  • You are somewhat familiar with the setup and know about webpack configurations and plugins and how to use them.

React with Shadow DOM

As you already know, the shadow root has to be created before we can utilize the Shadow DOM. If your project is anything like a typical React project, the code in the entry file — typically index.js — should look something like this:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css'; // css imports to be loaded into the project
import App from './App';
// ReactDOM render function
ReactDOM.render(<App />, document.getElementById('root'));

Create the Shadow DOM

To render our React app into the Shadow DOM, we will need to add some code to create the shadow root. Instead of writing the code directly above the ReactDOM.render function, let’s write it in a separate file.

/* createShadowRoot.js */const shadowHost = document.getElementById('root'); shadowHost.attachShadow({ mode: 'open' });

Next, we import createShadowRoot.js into file index.js like this:

import ReactDOM from 'react-dom';
import './createShadowRoot'; // must be imported above css files
import './index.css';

But why do we do this?
If you are familiar with how the css-loader and style-loader plugins work, you’ll know that during the webpack build process the plugins come into play and work their magic on CSS file imports. The style-loader plugin will insert code to inject <style> elements into the document <header>.

Since we want the styles to go into the Shadow DOM, we need to ensure that the Shadow DOM is first created so that styles can later be injected into it.

Inject styles into the Shadow DOM

Next, we want to tell the style-loader plugin to inject styles into the Shadow DOM. To do this, we need to customize the configuration settings for the plugin.

Begin by ensuring the style-loader package is up-to-date (version 0.22.0 or later). You can do this by running npm install style-loader@latest.

Add the configuration to inject styles into the shadow root. Your webpack configuration should look something like this:

rules: [
// Other webpack modules
...
{
test: /\.css$/,
use: [
{
loader: require.resolve('style-loader'),
options: {
insertInto: function () {
return document.getElementById('root').shadowRoot;
},
},
},
// Other css loaders (if any)
...
],
},
...
],

If you are using Create React App, you may have to perform npm run eject in order to be able to customize the style-loader configurations.

If you did it correctly, the imported stylesheets should now be loaded into the shadow root instead of the document <header>.

Render React into the Shadow DOM

All that’s left is to tell React to render the application into our newly created Shadow DOM. However, instead of simply rendering into the shadow root, we want to create another element within the Shadow DOM to render into.

Why not?
We do this so that React does not overwrite our <style> elements injected by the style-loader.

Add an element to the shadow root for React to render into in file index.js:

// Get the shadow root
const shadowRoot = document.getElementById('root').shadowRoot;
// Create div element for react to render into
const reactRoot = document.createElement('div');
reactRoot.setAttribute('id', 'react-root');
// Append react root to shadow root
shadowRoot.appendChild(reactRoot);
ReactDOM.render(<App />, reactRoot);

… and we’re all done! We now have a React app that works in the Shadow DOM! .. or not?

There is a known issue with certain events not firing for React when using Shadow DOM. One solution to the problem is to install the react-shadow-dom-retarget-events package.

Final notes

If you want your React components to have its own Shadow DOM, there’s a neat little package out there that does just that — check out ReactShadow.

Also, do check out some of the commonly discussed topics:

--

--