It's hard to overcome the troubles of global CSS

In the previous posts we had a look at the history of CSS frameworks and checked how you can use the current CSS techniques today to structure your styles better.

Next up is a glimpse of what the future of CSS might look like. A big part of the tooling around CSS focuses on fighting against the global nature of CSS, because in bigger projects that can quickly turn against you. Tools like CSS Modules are there already to help you, but there might be something better waiting just around the corner.

We'll have a look at a new technology called the Shadow DOM. Shadow DOM is a pretty new technology for scoping your styles. It's only supported by a limited number of modern browsers and the specifications (at the time of writing this) are still a bit unstable.

The "C" of CSS

So what is Shadow DOM and how can we use it?

To understand the technology, we'll need to have a look at the way browsers parse CSS. When you download a web page, it is first parsed into a tree structure called the Document Object Model – DOM. For example for a web site like this:

<!DOCTYPE html>
<html>
<head>
  <style>
  body {
    font-size: 10px;
  }
  p {
    background-color: #C0FFEE;
  }
  </style>
</head>
<body>
  <p>Hello world</p>
</body>
</html>

The Document Object Model would parse like this:

HTML
├─ HEAD
│  └─ STYLE
└─ BODY
   └─ P

Simple enough, right? After the CSS is parsed, the CSS is applied to the elements, creating a full CSS Object Model:

HTML
├─ HEAD
│  └── STYLE
└─ BODY
   ├ font-size: 10px;
   └─ P
      ├ font-size: 10px;
      └ background-color: #C0FFEE;

The browser works it's way from the top of the DOM, applying the styles recursively to the more specific branches of the Object Model. This is where the cascading comes to the Cascading Style Sheets: For example here the body's font-size CSS property cascades to it's child p tag, although the p doesn't have the font-size described.

Creating contained styles

When you're in full control of your website, the cascading property of the CSS is fine: You can use CSS Modules or BEM to avoid any conflicts in the inheritance. And you probably can control which "base" CSS framework or reset you are using as the base of the CSS Object Model.

But let's have a look at an interesting corner case: Let's say you host a website that sells tickets to local artists' concerts. Business is running fine, but some of the bands would like to add a button to their website, so their fans could buy the tickets from the artist's website.

"Easy enough" you think: You'll create a small JavaScript snippet that creates the button to any website it's inserted in. The code should look something like this:

const button = document.createElement(
  'button'
)
button.addEventListener(
  'click',
  openPurchasePopup
)
button.innerText = 'Buy tickets!'
const target = document.getElementById(
  'buy-ticket-button'
)
target.appendChild(button)

This assumes you have an element with the id buy-ticket-button in the target site's HTML, and it inserts the buy button inside the buy-ticket-button element.

We could also insert the button's styles to that page, for example with injecting a <style> tag with the CSS to the page:

const style = document.createElement(
  'style'
)
style.innerHTML = `
#buy-ticket-button button {
  width: 100px;
  height: 30px;
  border-radius: 15px;
  background: #C0FFEE;
  border: 0;
  color: #000;
}
`
target.appendChild(style)

This inserts a simple <style> tag to the website, that controls the styles of our buy button.

But what happens if the page has some of it's own styles that conflict with the styles that we'd want to display? For example, what if the target website has a style declaration for the button's font-family:

button {
  font-family: "Comic Sans";
}

Since we didn't determine the font for our button, the containing document's font-family declaration also affects our <button> element. What could we do to isolate our button's styles so the containing website couldn't change it?

We could of course declare all the available CSS properties to the button component. This has a couple of disadvantages: First, the size of the CSS we'd need to inject grows huge and second it's still possible to override even by accident. Of course we could apply !important to all the CSS lines so they would be harder to override, but this again makes the situation even worse.

The second option would be to inline all styles with the HTMLButtonElement.style property. This makes the amount of possible conflicts smaller, but still leaves room to some nasty edge-cases where the wrapping page uses !important for its styles.

The third option would be to create an <iframe> wrapper for the button:

const iframe = document.createElement(
  'iframe'
)
target.appendChild(iframe)
const iframeDoc =
  iframe.contentWindow.document
iframeDoc.body.appendChild(button)
iframeDoc.body.appendChild(styleTag)

This isolates the DOM and CSSOM completely, and we're guaranteed to have none of the CSS styles overridden. Yet there is one problem with this approach: We'd need to also style the <iframe> itself (since it has some borders by default). Also resizing the iframe element by its content requires some black magic, which leads us to even more problems.

Introducing the Shadow DOM

What if there was a fourth option that would isolate the CSS Object Model like when using an <iframe>, but which would have no styles and would be able to resize itself like a "normal" HTML element?

There is. Kind of.

This is a new browser standard called the Shadow DOM. The standard is still not stable and it runs natively only on Chrome. Basically instead of writing

const iframe = document.createElement(
  'iframe'
)
target.appendChild(iframe)

You create a wrapper called Shadow DOM:

const shadowDom = target.createShadowRoot()

This behaves very much like a regular <iframe>, but with one major difference: It does not have its own HTMLDocument or Window like an <iframe> does.

An <iframe> works by creating a new HTMLDocument and Window to encapsulate its content, so the content inside the frame cannot access the container document by default. Vice versa the <iframe>'s surrounding DOM render does not have any clue what's happening inside the frame. The frame usually has a fixed width and height.

But ShadowDOM only creates a new Document Object Model and encapsulated CSS Object Model for all the elements you insert inside the ShadowDOM. Since we don't have a separate Window, we don't need to use magic anymore to change the width or height of the wrapping content.

The specific behavior of the ShadowDOM might be hard to wrap your head around: The CSS encapsulation does not mean that any CSS could not go inside or outside the ShadowDOM – because some styles will cascade down into the ShadowDOM like they would cascade to normal HTML elements. ShadowDOM only encapsulates all the CSS styles that are described inside the ShadowDOM so they cannot effect the world outside the ShadowDOM.

Let's have a look at an example, shall we? Let's say we have the following style sheet for our document:

body {
  font-family: "Comic Sans";
}

This declaration creates a CSS Object model like the following:

HTML
└─ BODY
   └ font-family: "Comic Sans";

For our button we'd have the following styles:

button {
  width: 100px;
  height: 30px;
  border-radius: 15px;
  background: #C0FFEE;
  border: 0;
  color: #000;
}

Now what happens if we crate a <div> element to contain a ShadowDOM and set the button to be inside the ShadowDOM also? Well, the unfortunate font-family: "Comic Sans"; will cascade right through the Shadow DOM:

HTML
└─ BODY
   ├ font-family: "Comic Sans";
   └─ DIV
      └─ #shadow-root
         └─ BUTTON
            ├ font-family: "Comic Sans";
            ├ width: 100px;
            ├ height: 30px;
            ├ border-radius: 15px;
            ├ background: #C0FFEE;
            └ border: 0;

The good news is, that the <button>'s styles won't escape the shadow root; so even though we have declared styles for global button {}, the declarations inside the ShadowDOM won't have effect on the <button> elements outside the ShadowDOM:

HTML
└─ BODY
   ├ font-family: "Comic Sans";
   ├─ DIV
   │  └─ #shadow-root
   │     └─ BUTTON
   │        ├ font-family: "Comic Sans";
   │        ├ width: 100px;
   │        ├ height: 30px;
   │        ├ border-radius: 15px;
   │        ├ background: #C0FFEE;
   │        └ border: 0;
   └─ BUTTON
      └ font-family: "Comic Sans";

Vice versa if we declare the button {} CSS declaration outside the ShadowDOM, that declaration won't have effect to the <button> element inside the ShadowDOM:

body {
  font-family: "Comic Sans";
}

button {
  color: red;
}

So if we apply this style declaration the previous DOM, we would have the CSS Object Model that would look like this:

HTML
└─ BODY
   ├ font-family: "Comic Sans";
   ├─ DIV
   │  └─ #shadow-root
   │     └─ BUTTON
   │        ├ font-family: "Comic Sans";
   │        ├ width: 100px;
   │        ├ height: 30px;
   │        ├ border-radius: 15px;
   │        ├ background: #C0FFEE;
   │        └ border: 0;
   └─ BUTTON
      ├ font-family: "Comic Sans";
      └ color: red;

So the CSS selectors inside ShadowDOM won't have effect on elements outside the ShadowDOM and the CSS selectors outside ShadowDOM only have effect indirectly on elements inside ShadowDOM.

Can I use Shadow DOM?

The ShadowDOM is not yet quite usable in practice. Google's Polymer project uses a shim they call ShadyDOM to emulate the effects of ShadowDOM. But how about a full polyfill? Well, like the Polymer's developers said:

To polyfill shadow DOM turns out to be hard.

The full behavior is hard to polyfill, resulting in many nasty hacks to change older browsers' native features to match the new ShadowDOM spec. So ShadyDOM is the best tradeoff between matching the spec and producing a clean implementation of the wanted features that work by the rules of the older browsers.

So if you need to support any version of IE, you won't be using the ShadowDOM any time soon.

Takeaways

Sometimes you need to create components for webpages that you have absolutely no control over. Social media buttons and embedded posts are a great example of this, and most commonly you find them using a variety of hacks to keep the containing page from styling the components.

The best bet even today is (unfortunately) to use an <iframe>. It's the only technique that guarantees no possibility of any conflicts between the styles, but using an <iframe> has its own downsides.

A new web standard called Shadow DOM can help us to create contained styles without many of the <iframe>'s issues, but currently it comes with its own quirks for you to tackle.

While using Shadow DOM might not be the best practice today, we have seen standards and browsers evolve quickly when there's a need for a solution. In the future it just might be that Shadow DOM helps you to write local, maintainable CSS.

In the next blog post we're jumping from styling your website into styling your code. There are many modern tools you can use to help you write and maintain a clean codebase, whether you're writing CSS or JavaScript. Subscribe to the mailing list below to receive a notification when the new blog posts are out!

Read the next blog post here:

How to write better code by using a linter

Tweet

Be the first to know from new blog posts

Subscrbe to the mailing list to get priority access to new blog posts!