Shadow DOM
About six years ago I gave a little talk on shadow DOM in the front-end guild at Randstad (for whom I was working freelance at that time). Last week I was asked to give a similar talk at my current employers front-end guild because we had recently switched some of our components to shadow DOM due to style bleeds.
So I dusted off some old example code I had made way back, noticed there were some significant changes, read up on the current state, and thought it would make a nice post.
So why and what is shadow DOM?
In ancient times when websites were just starting to get larger, people started noticing annoying differences: new additions would cause unexpected changes to existing elements.
These side effects were dubbed style bleeds. CSS inheritance and specificity made this an annoying problem, riddling many stylesheets with repeated selector hacks and !important
.
People came up with (strict) styling strategies to combat style bleeds; OOCSS, BEM, SMACSS, ITCSS to name a few. They come with the added benefit that they also help structuring components semantically.
Speaking of components; they are a major advantage front-end frameworks brought us. In due time all major front-end frameworks would add some form of CSS scoping, rendering all those styling strategies more or less obsolete (although they do add logical structure).
But what these frameworks were really anticipating was shadow DOM: a technique that allows encapsulation in DOM and CSSOM.
What does shadow DOM really do?
Contrary to what you might think: shadow DOM does inherit CSS from its parent nodes. What the parent cannot do is target elements in the shadow DOM directly. Conversely, the CSS inside the shadow DOM has no effect whatsoever on the rest of the document.
There are however several ways we can control shadow DOM from outside: through the host selector, with slots, parts and CSS properties.
We used to have the selectors ::shadow
and /deep/
, but these were deprecated in favor of JS manipulation.
Example
Below is a working example that features these techniques (click the top right icon for editable source):
Custom elements
At this point it might be a good time to mention custom elements. Which is something different, but it generally goes hand in hand with shadow DOM. Yes, you can simply say
document.querySelector('#host')
.attachShadow({ mode: 'open' })
.appendChild(document.createTextNode('I am the terror that flaps in the night'))
But there's no fun in that.
host element, selector and function
The :host
pseudo-class selector is used from inside the shadow DOM to style the host element. It can be used to combine the outside- with the inside state. The host element itself is not really part of the shadow DOM; it can be styled by both document and shadow. So don't put any styling in there you don't want overridden.
For example: when a class is set onto the host element, you can use the host selector function inside the shadow DOM styling like this :host(.host-class-name) .inner { color: #F04; }
:
Since shadow DOM does inherit CSS rules, a common rule inside the shadow DOM styling is all: initial;
, and only let specific properties inherit. Like a sieve:
:host {
all: initial;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
One thing to remember is that outside styling of the host element takes precedence over host styling from within the shadow DOM.
slots
Slots can be seen as bubbles through which outside elements and styling can be placed inside the shadow DOM. Slots are the interface through which content can be placed in the shadow DOM.
A custom element can have multiple, named slots. The way to style it inside the shadow is by its name: ::slotted([slot=name]) { color: lime; }
.
Since slots are implemented (not defined) outside the shadow they can also be styled from without: [slot=name] { font-style: italic; }
.
parts
Parts are a way for the shadow element to designate specific areas to be accessible for styling from the outside. Inside the shadow you say <span part="label">Hello</span>
which makes it possible for the document stylesheet to have ::part(label) { color: lime; }
.
CSS properties
CSS properties are unaffected by shadow. All properties defined in :root
are accessible in shadow DOM. This also makes it possible to specify properties on the host element, as a more restrictive 'parts' implementation.
JavaScript
There use to be a way to pierce through the shadow with CSS, but that was deprecated because you really shouldn't want to. JavaScript is the way to go if you really must have access. You might want to test an effect for instance. All you really need is access the shadowRoot
property, and from there on out you can proceed inside the shadow as you would in your normal documentElement
root.
const myShadow = document.querySelector('my-shadow')
const {shadowRoot} = myShadow
const innerElement = shadowRoot.querySelector('.inner-element')
innerElement.style.color = '#f04'
Thats it
You'll probably never use all this because your framework takes care of shadow DOM for you. But at least now you now know its not really magic.