Can The CSS: Part Pseudo-Selector Be Used to Style Nested Web Components

Can the CSS :part pseudo-selector be used to style nested web components?

Nope. It is not possible. It kind a breaks the encapsulation principle. The right way is to use proper theming. That means using a combination of:

::part - For direct theming of the component
:host-context - for theming based on the context
::slotted - For styling the slotted element inside the styling

For more dynamic theming, you can use above styles in combination with Element.matches API. Whatever the class/context that the user of the component has set, you can then change the styling of nested children component.

On a side note, modifying the styling of a decadent component (children of children) is a bad practice even when not using Shadow DOM or Web Components. It will result in a brittle non-maintainable CSS.

Edit Note:

:host-context is not implemented in all browsers and probably never will.

::slotted CSS selector for nested children in shadowDOM slot

styling ::slotted elements in shadowDOM

Sample Image

TL;DR

  • ::slotted Specs: https://drafts.csswg.org/css-scoping/#slotted-pseudo

  • slotted content remains in light DOM, is reflected to a <slot> in shadow DOM

  • ::slotted(x) targets the lightDOM outer-Element (aka 'skin'), NOT the SLOT in shadowDOM

  • ::slotted(x) takes basic selectors

  • Inheritable styles trickle into shadowDOM

    https://lamplightdev.com/blog/2019/03/26/why-is-my-web-component-inheriting-styles/

  • For the latest WHATWG discussion on SLOT and related topics, see

    • https://github.com/whatwg/html/issues/6051#issuecomment-816971072

      Participants: rniwa (Apple) , annvk (Mozilla), dominic (Google)
    • https://github.com/WICG/webcomponents/issues/934#issuecomment-906063140

Interesting reads:

  • A history of the HTML <slot> element by Jan Miksovsky

  • Summary of positions on contentious bits of Shadow DOM — Web Components F2F on 2015-04-24


background

Yes, ::slotted() not styling nested elements is expected behavior.

The term slotted is counterintuitive,

it implies element lightDOM is moved to shadowDOM

slotted lightDOM is NOT moved, it remains.. hidden.. in lightDOM

the content (IF slotted) is reflected to a <slot></slot>

Or from Google Developer Documentation

, .

' ; .

I use the term reflected instead of render because render implies you can access it in shadowDOM.

You can not, because slotted content isn't in shadowDOM... only reflected from lightDOM.


Why :slotted has limited functionality

More advanced shadowDOM styling was tried.

WebComponents version 0 (v0) had <content> and ::content; but it was removed from the spec:

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/content

The main takeway from the W3C standards discussions

(@hayatoito (Google team) here and here) is:

Sample Image

So in V1 we have :slotted: https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted


Addition #1 : Performance if ::slotted allowed for complex selectors

From Mozilla developer Emilio:

source: https://github.com/w3c/webcomponents/issues/889

The performance issue is that it increments the amount of subtrees in
which every node needs to go look for rules that affect to them.

Right now the logic goes like: if you're slotted, traverse your slots
and collect rules in their shadow trees as needed. This is the code
This is nice because the complexity of styling the element
depends directly on the complexity of the shadow trees that you're
building, and it only affects slotted nodes.

If you want to allow combinators past slotted then every node would
need to look at its ancestor and prev-sibling chain and look at which
ones of them are slotted, then do that process for all their slots.
Then, on top, you also need to change the general selector-matching
code so that selectors that do not contain slotted selectors don't
match if you're not in the right shadow tree.

That's a cost that you pay for all elements, regardless of whether you
use Shadow DOM or ::slotted, and is probably just not going to fly.


So due to performance issues

:slotted( S ) got limited CSS selector functionality:

  • ► it only takes simple selectors for S. --> Basically anything with a space won't work

  • ► it only targets lightDOM 'skin'. --> In other words, only the first level

<my-element>
<h1>Hello World</h1>
<p class=foo>
<span>....</span>
</p>
<p class=bar>
<span>....</span>
</p>
</my-element>
  • ::slotted(h1) and ::slotted(p) works

  • ::slotted(.foo) works

  • ::slotted(span) (or anything deeper) will not work (not a 'skin' element)

Note: ::slotted([Simple Selector]) confirms to Specificity rules,

but (being simple) does not add weight to lightDOM skin selectors, so never gets higher Specificity.

You might need !important in some (rare) use cases.

 <style>
::slotted(H1) {
color: blue !important;
}
<style>

Styling slotted content

Also see: Applying more in depth selection to the :host CSS pseudo class

#1 - style lightDOM

The <span> is hidden in lightDOM, any changes made there will continue to reflect to its slotted representation.

That means you can apply any styling you want with CSS in the main DOM

(or a parent shadowDOM container if you wrapped <my-element> in one)

 <style>
my-element span {
.. any CSS you want
}
<style>

#2 - (workaround) move lightDOM to shadowDOM

If you move lightDOM to shadowDOM with: this.shadowRoot.append(...this.childNodes)

you can do all styling you want in a shadowDOM <style> tag.

Note: You can not use <slot></slot> and :slotted() anymore now.

<slot>s only works with content reflected from lightDOM.

For an example where an element wraps itself in an extra shadowDOM layer,

so no CSS bleeds out, and <slot>s can be used, see:

  • https://jsfiddle.net/WebComponents/5w3o2q4t/?slotmeister

#3 - ::part (shadow Parts)

It is a different/powerful way of styling shadowDOM content:

Apple finally implemented shadowParts in Safari 13.1, March 2020

see:

  • https://meowni.ca/posts/part-theme-explainer/

  • https://css-tricks.com/styling-in-the-shadow-dom-with-css-shadow-parts/

  • https://dev.to/webpadawan/css-shadow-parts-are-coming-mi5

  • https://caniuse.com/mdn-html_global_attributes_exportparts

Note! ::part styles shadowDOM,

<slot></slot> content remains in lightDOM!


references

be aware: might contain v0 documentation!

  • https://css-tricks.com/encapsulating-style-and-structure-with-shadow-dom/

  • https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=en#composition_slot

  • https://polymer-library.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-your-elements

  • https://github.com/w3c/webcomponents/issues/331

  • https://github.com/w3c/webcomponents/issues/745

  • https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event

  • ::part() - https://developer.mozilla.org/en-US/docs/Web/CSS/::part


Example: Using slots as a router

Change the slot-name on buttonclick and reflect content from lightDOM:

<template id=MY-ELEMENT>
<style>
::slotted([slot="Awesome"]){
background:lightgreen
}
</style>
<slot><!-- all unslotted content goes here --></slot>
<slot id=answer name=unanswered></slot>
</template>
<style>/* style all IMGs in lightDOM */
img { max-height: 165px;border:3px dashed green }
img:hover{ border-color:red }
</style>
<my-element><!-- content below is: lightDOM! -->
SLOTs are: <button>Cool</button> <button>Awesome</button> <button>Great</button>
<span slot=unanswered>?</span>
<div slot=Cool> <img src="https://i.imgur.com/VUOujQT.jpg"></div>
<span slot=Awesome><b>SUPER!</b></span>
<div slot=Awesome><img src="https://i.imgur.com/y95Jq5x.jpg"></div>
<div slot=Great> <img src="https://i.imgur.com/gUFZNQH.jpg"></div>
</my-element>
<script>
customElements.define('my-element', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode:'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
this.onclick = (evt) => {
const label = evt.composedPath()[0].innerText; // Cool,Awesome,Great
this.shadowRoot.getElementById("answer").name = label;
}
}
});
</script>

What is the ::content/::slotted pseudo-element and how does it work?

The ::content pseudo-element is being replaced in future implementations of Web Components / Shadow DOM with the ::slotted pseudo-element. Likewise, the element targeted by this pseudo-element has changed from <content to <slot> in the latest version of the Shadow DOM specification. You can see related discussion about that change here.

Currently browsers still support <content> and ::content.



Original answer:


Summary:

::content is essentially a way to dig deeper and style descendants of the ShadowHost, which normally aren't available to be styled, because your CSS doesn't know to look for the ShadowDOM fragment without ::content.


This answer assumes you are at least somewhat familiar with the <template> element and Web Components, specifically the ShadowDOM, which deals with ShadowTrees and their two main elements, ShadowHost and ShadowRoot.

Note - As of this writing, there is less than 50% support (even prefixed, off-by-default support) for Web Components across the five major browsers. While all modern browsers support <template>, only recent versions of Chrome and Opera support the ShadowDOM fully; with Firefox supporting parts of it after you toggle the requisite feature in about:config (dom.webcomponents.enabled) to true.

The goal of using the ShadowDOM is similar to MVC's separation of concerns. That is, we want to separate our content from our presentation and allow for encapsulated templates in our code to help make it more manageable. We have this already in various programming languages, but it's remained a problem for some time in HTML and CSS. Further, there can be conflicts with class names when styling elements in web apps.

Normally, we interact with the LightDOM (a sort of "Light Realm"), but sometimes it would be helpful to take advantage of encapsulation. Crossing into this sort of "Shadow Realm" (part of Web Components) is a new method to prevent the problems mentioned above by allowing encapsulation. Any styles applied to markup in your ShadowTree won't apply to markup outside of your ShadowTree, even if the exact same classes or selectors are used.

When the ShadowTree (which lives in the ShadowDOM) has a tree from the LightDOM distributed within it, and/or when the ShadowTree is rendered, the result is converted by the browser into what is called a composed tree.

When the browser renders your code, content is being distributed and inserted at new locations other than where it was physically typed. This distributed output is what you see (and what the browser sees), and is called the composed tree. In reality, the content is not originally typed in the order that it now appears, but you won't know this, and neither will the browser. This separation between "end result" and "original code", if you will, is one of the main benefits of encapsulation.

Web Components & the Future of CSS is a great 40-minute video on Web Components and specifically the ShadowDOM, pointed out to me by ZachSaucier.


Specific to your question, the ::content pseudo element applies to what are called distributed nodes. A distributed node is another term for whatever you put within the <content></content> tags. The content is distributed from its place in the original markup to wherever you have placed your <content> tags in the template.

So, when you need specificity in CSS, one way you can handle selectors normally is that you go to the parent element and add that in as part of the selector. Ex: if .container {} is not specific enough, you might use div .container {} or .main .container {} in order to make your selector work.

Thinking about the point of the ShadowDOM, which is scoping and encapsulation, you have to realize that this new ShadowTree you've created is a completely new (discrete) DOM fragment. It's not in the same "Light Realm" as the rest of your content; it's in a "Shadow Realm". So, how does the CSS know to target this "Shadow Realm"? By using the ::content pseudo-element!

The ::content pseudo-element selector acts as the parent element of distributed nodes.

HTML5Rocks has a great sequence of tutorials here, here, and here which cover more information and give some great examples (be sure to visit with Chrome or Opera until more browsers support these features).

For example, see this modified and improved (by Leo) version of the code from HTML5Rocks:

var div = document.querySelector('div');
var root = div.createShadowRoot();
var template = document.querySelector('template');

root.appendChild(template.content);
<template>
<style>
h3 { color: red; }
content[select="h3"]::content > h3 { color: green; }
::content section p { text-decoration: underline; }
</style>
<h3>Shadow DOM</h3>
<content select="h3"></content>
<content select="section"></content>
</template>

<div>
<h3>Light DOM</h3>
<section>
<div>I'm not underlined</div>
<p>I'm underlined in Shadow DOM!</p>
</section>
</div>

How to position nested shadow DOMs on top of each other?

It's possible. In the shadow DOM, the following should be added:

:host { 
position: absolute;
top: 0;
}

This :host rule styles all instances of the custom component element (the shadow host) in the document to be absolutely positioned, while also making the positioning relative to each positioned parent (that's why top: 0 is needed).

In fact, this is similar to changing the normal DOM code in the question to use positioning (note that without positioning, just setting top: 0 won't work (further read)):

.container {
position: relative;
}
.container div {
position: absolute;
top: 0;
}
.d1 {
width: 150px;
height: 150px;
background-color: lightseagreen;
}
.d2 {
width: 100px;
height: 100px;
background-color: darkslateblue;

}
.d3 {
width: 50px;
height: 50px;
background-color: lightgray;
}
<div class="container">
<div class="d1">
<div class="d2">
<div class="d3">
</div>
</div>
</div>
</div>

How to access elements inner two shadow dom

You can use the ::part pseudo-element and exportparts attribute for this. It allows you to make custom styles for inner Shadow DOM element.

You can specify a "styleable" part on any element in your shadow tree:

<x-select>
:shadow-root
<header class="select-header"></header>
<div class="select-container" part="container"></div>
</x-select>

And then you can specify custom styles for that part like:

x-select::part(container){
font-weight: bold;
}

Also it works with other pseudo selectors like :hover, :active...

x-select::part(container):hover {
opacity: 0.8;
}

But it doesn't work with nested parts. So, you cannot use it like:

x-select::part(container)::part(aside) {
}

For that purpose you need to export the part via exportpart attribute.

<x-bar>
:shadow-root
<x-foo exportparts="some-box: foo-some-box"></x-foo>
</x-bar>

However, if you need to support IE11 then it might be not the best option. From the other hand, it's supported by all modern browsers: https://caniuse.com/#search=%3A%3Apart

So, your example would look like:

// x-select

render () {
return (
<Host>
<div class="select-container" part="container"></div>
</Host>
)
}
// x-typography

render () {
return (
<Host>
<x-select exportparts="container: container"></x-select>
</Host>
)
}

<!-- usage -->

<x-typography></x-typography>

<style>
x-typography::part(container) {
color: blue;
}
</style>

Here you can find a great explanation how part works: https://github.com/fergald/docs/blob/master/explainers/css-shadow-parts-1.md

CSS: How to target ::slotted siblings in Shadow DOM root?

Sure you can select siblings of slots / slotted.

The thing you can not do is select a element which has been slotted and is not a top-level node.

Select siblings:

slot[name=<slotname>] ~ <selector>

Select slotted top-level node

::slotted(<compound-selector>)

A compound-selector contains a tag/class/id/name etc. but must not have any combinators. Like <space> for example.

.myClass OK

<anyTag>[<anyAttribute>[=<anyValue>]] OK

.<myClass> > .<anotherClass> NO

Examples

var element = document.querySelector('.templateMe');
var shadow = element.attachShadow({mode: 'open'});
var template = document.querySelector('.myTemplate');
shadow.appendChild(template.content.cloneNode(true));
<template class="myTemplate">
<style type="text/css">
::slotted([slot=slot1]) { /* slot1 every slotted element - YES */
color: red;
}

slot[name=slot1] { /* slot1 itself - YES */
text-decoration: underline;
}

slot[name=slot1] + .siblingA { /* slot1 siblingA (direct sibling) - YES */
color: green;
}

slot[name=slot1] ~ .siblingB { /* slot1 siblingB (any sibling) - YES */
color: orange;
}

slot[name=slot2]::slotted(.selectMeA) { /* slot2 TOP-LEVEL CHILD (slotted) - YES */
color: purple;
}

slot[name=slot2]::slotted(.selectMeB) { /* slot2 NOT TOP-LEVEL CHILD - NO */
font-weight: bold;
}

slot[name=slot2]::slotted(.selectMeC[name=myName]) { /* slot2 TOP-LEVEL CHILD (slotted) - YES */
color: khaki;
}

slot[name=slot2] + .siblingC { /* slot2 sibling - YES */
color: blue;
}

</style>
<div>
<slot name="slot1"></slot>
<div class="siblingA">Sibling A of Slot 1</div>
<div class="siblingB">Sibling B of Slot 1</div>
</div>
<hr/>
<div>
<slot name="slot2"></slot>
<div class="siblingC">Sibling C of Slot 2</div>
</div>
</template>

<div class='templateMe'>
<span slot="slot1">Im in Solt 1</span>
<span slot="slot2" class="selectMeA">
Im in Solt 2, im selectable.
<div class='selectMeB'>
NOT selectable (because no top level node of slotted)!
</div>
</span>
<span slot="slot2" class="selectMeC" name="myName">Im in Solt 2 too and selectable!</span>
</div>


Related Topics



Leave a reply



Submit