Shadow DOM is a fairly recent-ish spec that gives you DOM tree encapsulation â itâs one of the superhero lions in the Voltron of specs called âWeb Componentsâ. Web Components let you create reusable, self-contained components in JavaScript; the Shadow DOM bit makes sure that the CSS and markup you bundle with your implementation is encapsulated, hiding the implementation details of your element.
The idea of encapsulation isnât new â most programming languages have a way to define âprivateâ bits of code â variables or methods that are irrelevant to the user of that object and make the element work. Messing with them usually voids the contract and breaks the guarantee that the element will continue to work. In these languages you could, instead, use a global variable or method for everything. Itâs not a question of whether it will work (it will), but whether it will work over time, in a large code base (it wonât). You know it wonât.
On the web, thereâs two kinds of encapsulation we might want: style encapsulation (an elementâs styles donât leak outside) and DOM encapsulation (an elementâs internal implementation isnât visible). This post talks about style encapsulation; tune in soon for the second half of the story â the DOM encapsulation!
Whew, ok then. So then why is CSS encapsulation so hard? And whatâs the fastest way to get it?
Tools to the rescue!
đ Before you set me on fire on Twitter, hear this: the next paragraph isnât a criticism of CSS (which I think is the greatest tool for authoring styles) nor a criticism of the tools we use (which I think fill real gaps we have), but a criticism of the standards process itself.
I have a theory that developers will put up with too much when it comes to writing CSS. For a while there, CSS wasnât moving forward, so people started using tools to get around that. We didnât have variables or mixins, so we started using preprocessors. We didnât have style encapsulation, so we started naming things âthe right wayâ with BEM, so that we didnât accidentally stomp over each otherâs styles. We wanted to be able to author CSS from inside a JavaScript component, so we started using CSS-in-JS. We needed all these tools, because âthe platformâ (read: the browsers that be) wasnât there, and building these tools showed that there was a need to move forward. For style encapsulation, Shadow DOM is the platform moving forward.
The unsatisfying part of the web is that you donât have these problems when you build a one page site or app â you have control over your 17 shades of slightly different blue and your custom build pipeline. But when you have big projects, with weird architectures, targeting different platforms and written across different teams, you end up spending a lot of time just setting up infrastructure and build configurations, which kind of sucks.
Existing scoping approaches
So now that you (maybe) believe me that style encapsulation is a good thing, letâs talk about the bunch of ways in which you can get various degrees of it. They basically come in two flavours: encapsulation by convention or encapsulation with buy-in. Here they are (in my opinion), from least to most effective:
1. Better naming strategies
âName your stuff betterâ works if you have control over the things you are naming. But if you already do, then you probably donât need style encapsulation in the first place. You can justâŚnotâŚdo the bad things and the stomping. The problem is that if youâre building a third party widget (say, a fancy date picker that everyone in the universe will have to use), or if youâre building something as part of a large team, you have to be very, very careful not to name it anything that anyone out there might ever call it. Not very scientific.
đ Itâs really easy and doesnât need tools.
đ Itâs really hard if you donât have tools to enforce it. And doesnât really work.
2. <iframe>
Ugh, you know it works. Iframes are this special magical portal that teleports any piece of HTML into your piece of HTML, while keeping it wrapped in a safety bubble. But you canât resize them easily. Or scroll nicely. Or pretend theyâre not a teleported piece of code wrapped in a safety bubble. I didnât even have to doctor this screenshot, itâs real life:
đ Itâs the most encapsulation and abstraction you will ever get on the web.
đ Itâs an iframe.
3. CSS modules
CSS Modules are another approach to faking style encapsulation. Itâs basically a smart way of automating BEM, so that you donât have to worry about choosing the unique class names â thereâs a tool that does it for you! It works pretty well, since it prevents any potential name collisions youâve had with BEM, but at the end of the day, itâs not actually style encapsulation. Thereâs nothing stopping you from styling any bit of the DOM tree, which means itâs not a very satisfactory answer if youâre in the business of vending, or using, robust third party components.
4. CSS-in-JS
CSS-in-JS is a new approach that lets you author CSS literally in JavaScript. Then, this JavaScript is basically transmogrified into a style, which means that that style is sort of encapsulated â itâs local to that element, and hard to stomp over. Thereâs several ways to do this, some better than others:
Directly setting the style as an attribute
someElement.style.marginLeft = â20pxâ
This is the worst of all the worlds because the CSS parser can do way fewer optimizations and caching than if you used class names, for example (see a benchmark).
Embedding CSS style strings in your JS output
Something like <div style=â...â>
is still pretty terrible for performance. Browsers (or at least Chrome), do a looooooot of string conversions in this case, which means it at least doubles your memory footprint, because the same string has to live both in V8 and Blink. Hereâs what happens behind the scenes:
- Take the JS off the wire, in whatever encoding your page is in
- Turn it into whatever encoding V8 prefers, for super optimal memory compactness
- Scan the JavaScript string
- Parse the JavaScript string
- Turn it into an internal string for the DOM when you want to apply the styles
- Potentially re-encode it if youâre unlucky
- Take the internal string, pass it to Blink (string copies ahoy!)
- Blink passes it to the CSS parser, which turns it into styles
Compiling out your CSS
Like, into a separate resource, and then applying styles via classes. This works really well, since youâve used the browser as it wanted to be used. In comparison to the previous case, for a regular <style>
in a CSS stylesheet, the browser has the same string and just passes it around:
- Take the CSS off the wire into Blink
- Tokenize it
- Build a DOM tree with the string as a text node
- Parse the text node
- Pass it to the CSS parser, which turns it into styles
đ Managing a giant amount of styles is nice. Style encapsulation is nice. It works extremely well if youâre using a framework that works well with this.
đ Thereâs a million ways to do this, and itâs really overwhelming if you are new to it. This approach tends to also be married to a framework, which makes sharing components hard -- both the user and the author of a component need to agree on both the framework and the css-in-js style, which isnât always possible.
4. Shadow DOM
This is a cheap move: you know this article is about the Shadow DOM, and I left it until the end because I obviously think itâs the best. Shadow DOM was literally built to solve the problem of style and DOM encapsulation. It does the same thing that <input>
and <video>
elements have been doing for years (hiding their dirty laundry) but in a way that browsers can optimize around.
The reason for that is that browsers have a special style resolver for Shadow DOM trees. Apart from being regular CSS that the browser already knows how to optimize, the CSS inside shadow DOM trees only applies inside that element. This means that changing a class name or style inside of a shadow root wonât affect everything outside it. Since you donât have to consider the rest of the world, this means style resolution and application is much faster.
The same argument can be made for element authors â since you know that everything inside of your element canât leak outside, the implementation is much simpler. You donât have to think about the rest of the world. You only have to consider your elementâs public API, and its implementation.
Before you complain that using a Shadow DOM and Web Components means that it absolutely requires JavaScript: this is true. But if youâre in a big team, building the kind of big app where youâre looking to style encapsulation as a solution for your CSS bowl of spaghetti, Iâm pretty sure youâre already using JavaScript. And the community has been exploring solutions to server-side rendering Shadow DOM anyway. Tradeoffs be tradeoffs, and this seems like an easy one.
đ Weâve been complaining that nothing in CSS was helping with style encapsulation and this is literally the platformâs answer to that problem.
đ Because itâs a new spec, itâs suffering from some growing pains. On older browsers you need a polyfill. If you want reusable elements that are also highly customizable, this style encapsulation might get in the way right now. Thankfully, good people are already working on that. Custom properties are a new spec meant to address this, and the new proposal for theming custom elements is now an editor's draft!
The zen of web development is a small page â reusable components, not a lot of code, no wheels reinvented. Encapsulated styles are better for you as a developer (code can be simpler), and better for you as a platform (code can be faster). And without external tools or iframe nightmares, the only way to get this is Shadow DOM.