Matthew Miner's Basic-ish Blog

Sometimes I might say something

How to Scope CSS by Moving an Element to the Shadow DOM

January 29, 2019 PROGRAMMING WEB DEVELOPMENT HTML CSS JAVASCRIPT SHADOW DOM
Bakura demonstrating how to banish things to the shadow DOM in Yu-Gi-Oh!

(tl;dr: Jump to the actual moving)

Scope is an important concept in programming. It allows you to just write small sections of code to do what you need them to do without having to worry about something else in your code using the same name and messing everything up. Declarations (such as of variables or functions) will only be visible to code also in the same scope, usually some block of code.

MDN has a good example of the effects of scope in Javascript:

let x = 1;
if (x === 1) {
    let x = 2; // Not an error because it's in a different scope
    console.log(x); // Logs 2
}
console.log(x); // Logs 1

Sadly this sort of functionality has been sorely lacking in HTML and CSS. One has either had to carefully check that he doesn't reuse any class names on his site or one's styles could really mess up anothers. Special frameworks have even been written to dynamically generate class names to ensure there are no conflicts. This can work if you are fine learning the whole framework, taking on that overhead, and having all your elements having a bunch of these "col-md-offset-4"-like classes; but it's not always ideal. For a while there was the bright hope of the scoped attribute. This allowed one to do this:

<p>Hello unstyled world!</p>
<div>
    <style scoped>
        p {
            color: blue;
        }
    </style>
    <p>I'm blue (Da Ba Dee Da Ba Die)</p>
</div>

However, scoped was tragically snuffed out years ago due to Chrome refusing to implement it. Instead we got...the shadow DOM.

Technically, shadow DOM already existed. Browsers have long used shadow DOMs under the hood to style things like <input> sliders, but only very recently have users been able to use them. They are now implemented in all three major browsers, very useful, and...not very intuitive to use.

"What even is a shadow DOM?" you may ask. Well, a shadow DOM is basically a portion of your page that cannot affect anything elsewhere. It's encapsulated and therefore scoped. You can attach a special node called a shadow root to (almost) any node on your webpage, and you can then attach other elements to this shadow root. This shadow tree will render without affecting anything else. To actually implement this, one is required to use JavaScript for, uh, some reason probably. You do it like this:

<p>Hello unstyled world!</p>
<div id="shadow-host"></div>
<script>
    let shadowRoot = document.getElementById("shadow-host").attachShadow({mode: 'open'});
    let shadowRealm = document.createElement("div");
    shadowRoot.appendChild(shadowRealm);
    let bonz = document.createElement("q");
    bonz.innerHTML = "Ahhh help me";
    shadowRealm.appendChild(bonz);

    shadowRealm.style.backgroundColor = "purple";
    bonz.style.color = "transparent";
    bonz.style.textShadow = "0 0 1px white";
</script>

There, a working shadow DOM. I also added some styling on in there. That again has to be done with JavaScript...
Note that you can't actually style the shadow root itself. There's actually very little that you can do to it.

Of course, setting all CSS through JavaScript would be terrible. I don't even think you can style the shadow root through JS. Thankfully, even more recently, external stylesheets have been allowed to be linked within shadow DOMs, meaning one can now just do this to import a whole stylesheet just for a section of the page:

let shadowLink = shadowRoot.appendChild(document.createElement('link'));
shadowLink.href = "/style.css";
shadowLink.rel = "stylesheet";
shadowLink.type = "text/css";

For my use though, I wanted to generate the page server-side and just load the isolated CSS afterwards. For this, since JavaScript is inexplicably required, I just set a class on the things I want encapsulated and then looped through those with JavaScript and banished them to the shadow DOM:

<details class="banish-me">
    <summary>Open me!</summary>
    <p>Hello! :D</p>
</details>
<script>
    document.querySelectorAll('.banish-me').forEach(x => {
        let shadowHost = document.createElement('div');
        x.parentNode.replaceChild(shadowHost, x);
        let shadowRoot = shadowHost.attachShadow({mode: 'open'});
        shadowRoot.appendChild(x);
        let shadowLink = shadow.appendChild(document.createElement('link'));
        shadowLink.href = "https://www.w3.org/StyleSheets/Core/Midnight";
        shadowLink.rel = "stylesheet";
        shadowLink.type = "text/css";
    }
</script>

There, now this works nicely. This technique lets each <details> have its own external stylesheet loaded just in its scope. Of course, as long as the user has a new enough browser and has JavaScript enabled.

Previous Post