Web Components and You (part 5): Piercing the Shadow DOM
Published 2024-03-24, About 7 minute read.
In post 1 of this series we showed how the shadow Dom encapsulate styles. Basically, whatever is defined outside the shadow DOM doesn't intrude inside the web component's shadow root. You can then style inside the shadow DOM and not have a care about how this component will look in any context- the context shouldn't matter!
In the last post we showed a few options of including styles in your web components. We didn't delve into how we might have consumers interact with the shadow DOM's styles. But there are ways to influence styles inside the web component. I want to show a sketch of how to use each of them:
- The
:part()
API - CSS Variables (custom properties)
-
Using context-dependent properties like flex and grid and
display: contents
. - Inverting control and using slots
- Including styles with a declarative shadow DOM
Using the ::part()
selector
The ::part()
api allows you as a web-component maker to
selectively expose parts of your shadow DOM to be styled with CSS. To make
something available, you just need to name that part with the attribute
part="some-name"
Now this is available to be styled using ::part(container)
.
Let's see a full example of how we can do this:
You can see that the divs are not changed by the general div rule. But the part rule allows you to style that div and that div only.
You might be thinking "Oh noes! People now have a selector to get in the DOM, they can start selecting children, siblings, and more, and mess everything up!"
Never fear! This is not true!
You will quickly find that you cannot combine the part selector with other selectors to traverse the shadow DOM. The only thing that might work are the psuedo selectors on that custom element, such as :disabled
, :valid
, and so on.
Check this out: This shows how combinations of selectors with part don't allow you to traverse around:
So you really do have control over what you expose. I used this in my code-playgoround web component. I put part="editor-format-button"
on the format button. I want people to be able to style their buttons in a way that makes them match their site, but nothing more.
A really nice example of a library that exposes parts for you to customize is the shoelace library. They have a Nice explainer on parts
But there are some other things that actually go through the shadow DOM by default, let's move on to...
CSS Variables
Be default, CSS variables go right through shadow DOM boundaries.
If you want someone to only be able to change one particular aspect of your web component, say a border-color, or a text font-family, you can use css variables. The consumer can set them how they would like and it would affect your component.
Again, a really good example of a library that uses this strategy is shoelace
Context-dependent relationships like flex and grid
One thing I don't see touched on very much is that certain parent-child relationships can seep into the shadow DOM. For example, if you create a web component with a few items, and the host is display: contents
, a containing grid or flexbox will consider the shadow DOM contents as children. Here's an example:
Since the flex is outside of the custom element, you wouldn't expect those styles to impact the divs in the shadow DOM. But in this case the layout considerations of flex
, which would normally have targeted `my-element` now target its children because of display: contents
.
To me this is more of a curiosity than a strategy to reach into the shadow DOM. I think you could rely on this if you were to explicitly say that consumers need to use grid or flex to style the elements in the component. For example, you may want the user to be able to set display: grid
on the host custom element and then dictate the layout of children. Something like this:
But then again it doesn't seem very wise to rely partly on styles on the top element. Many CSS styles placed there won't work, for example, child selectors will fail, like this:
So you would be left to exposing those child elements with part
or handle the styling internally. Personally, I think I would handle all this internally and expose parts as I find the consumer might need them.
Inverting control using slots
One interesting thing about slots in the shadow DOM is that slotted elements are styled according to what styles would reach them in the light dom before being included in the shadow DOM. Here's an example:
So, if you need a consumer to bring their own button for example, instead of exposing your own button with part
or exposing tons of custom properties the consumer needs to set, you can just have them place their own styled button in a slot. The styles will get rendered on the element as if it were outside the shadow DOM, but it would be located in the correct spot in your component.
You would need to hook up the button manually, but this is probably a more advanced topic for a future post!
Lastly: Including styles with a declarative shadow DOM
This is the wildest and most experimental example of style injection into the shadow DOM. Last post we saw how to adopt global styles into the shadow DOM programatically. There is a much shorter way you can do this and in a way "pierce" the shadow dom by setting up a declarative shadow dom with the styles already placed in it.
Declarative shadow DOM is also probably a great topic to go over also in a different post. I consider this to be a more advanced strategy requiring a lot more coordination between template, web component, and styles, and to be honest, we're still figuring out how to use this shadow DOM thing. So take this example with a grain of salt.
But here it is. If you have a template inside of the markup of a web component with attribute shadowrootmode
set to either open or closed, the web component will have a shadow DOM already instantiated when it is loaded on the page.
Alright, let's show an example. I'm hosting a css file here that I'll be using in this playground example. I'll load it in the main body of the page, but also in the web component shadow DOM using this declarative strategy.
This is actually quite easy to include tailwind styles everywhere if we're heavily invested in web components and want to use tailwind. The downside, again, is that you would need to set up tailwind to look through your templates and your web components for all the css classes to include.
But tailwind aside, this is a legitamite way you might consider preloading custom elements with the styles and other things you need globally. It just takes a bit of work to systemize it.
Conclusion
Web components are really flexible, and with the addition of the declarative shadow DOM there are many ways to get styles into that shadow DOM.
Hopefully you've been inspired to try some of these strategies creating your own components. Did I miss anything? What do you think? Leave a comment below!
I think in the next post we'll talk a bit more about slots, how to interact with them, and what they enable us to do with Web Components. Until then, peace and happy programming!