Borrowing some ideas from https://github.com/w3c/webcomponents/issues/883, I think it would be great to allow combinators after ::slotted(whatever), for example ::slotted(.foo) .bar > .lorem + ip-sum.
Here's a live example showing that it currently doesn't work:
https://stackoverflow.com/questions/62842418
On the same token, being able to use these selectors in querySelector and querySelectorAll would be very convenient.
Here's a live example that shows that querySelector('::slotted(.foo)') doesn't work despite being a valid selector, and also showing that postfixed combinators throw an error:
https://codepen.io/trusktr/pen/d4a45f1efc9eccc0fdb20164566bada4?editors=1010
Having these features as expansions on the current spec would allow what the OP in #883 asks for.
For those reading email, sorry, I submitted the thread early without any content, and edited the original post.
What does the ::slotted syntax add that the querySelector("[slot].foo") would not cover? Slotted content is light DOM so you shouldn't need to run that query inside shadowRoot.
Wouldn鈥檛 querySelector('::slotted(.foo)') give away that a shadow DOM exists (even in a case where the shadow root is closed)? It seems to me like host.querySelector('.foo') should suffice from a DOM API perspective although I agree the CSS selector from within a shadow root would be a nice addition if the performance worked out.
@castastrophe Thanks for that question! It's not quite what I'm asking about though.
The ::slotted selector runs within a shadow root context (f.e. in a shadow root querySelector, or in a shadow root's <style> element), but this is already expected behavior. More details...
const root = this.attachShadow(...) // 'this' is a custom element
root.innerHTML = html`
<style>::slotted(.foo) { ... }</style>
<slot></slot>
`
The ::slotted(.foo) selector in this example is in fact still be an implementation detail encapsulated inside the (custom element's) shadow root.
The [slot].foo selector does a _similar_ thing, but does it from the outside of the custom element (i.e. outside of the shadow root), so it is a tool used on the outside of a custom element's internal shadow tree.
The custom element itself could in fact run this code: this.querySelector('[slot].foo'), but the style of the shadow root actually can't contain such a selector as it would not select anything. For example, this won't work the same way:
this.attachShadow(...).innerHTML = `
<style>[slot].foo { ... }</style>
<slot></slot>
`
because it will not select nodes from the light tree that have been "slotted" into the shadow root; instead it will select nodes that are within the shadow root's own DOM that have a slot="" attribute.
in other words, calling this.querySelector('[slot].foo .bar') on a custom element selects any elements in the light tree with a slot attribute _even if they are not actually slotted_.
So inside of a custom element's implementation (in its shadow root), the ::slotted(.foo) selector is be useful for selecting _actually-slotted_ elements from the light tree, which is something that only the custom element (its shadow root) would care about (otherwise, as @calebdwilliams mentioned, the user would be aware of the shadow root's existence if they could run that selector on the custom element instead of the custom element's (possibly closed) shadow root).
END DETAILS
Anywho, all of the aforementioned is already an existing concept.
The main ask of the OP is to be able to take a selector like ::slotted(.foo) (which already works, within a shadow root) and to be able to append combinators to it. For example ::slotted(.foo) .bar or ::slotted(.foo) > .bar.
As an example, try running the following in your console (tested in Chrome).
What you'll notice is that the square will be colored deeppink, but the expected result is for the square to be cyan.
The reason is because the ::slotted(.foo) selector is working fine, but the ::slotted(.foo) .bar selector doesn't do anything (unlike what we may expect).
class MyEl extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({mode: 'open'})
root.innerHTML = /*html*/ `
<style>
:host { display: block; }
::slotted(.foo) { /* This works. */
background: deeppink;
}
::slotted(.foo) .bar { /* This does not work. */
background: cyan;
}
</style>
<slot></slot>
`
}
}
customElements.define('my-el', MyEl)
document.body.insertAdjacentHTML(
'beforeend',
/*html*/ `
<my-el>
<div class="foo">
<div>
<div class="bar"></div>
</div>
</div>
</my-el>
<style>
my-el {}
.foo {
width: 50px;
height: 50px;
}
.foo div {
width: 100%;
height: 100%;
}
</style>
`,
)
Expected result:

Actual result:

^ I added screenshots of the expected and actual results.
Ah, thank you for the code example. This is not really about the ::slotted selectors then. This is about the fact that a web component cannot style or access any descendent greater than a direct child, top-level nodes inside slotted content. Your .bar class is nested, which is why ::slotted cannot influence it. https://developers.google.com/web/fundamentals/web-components/shadowdom#slots
That is not to say that is not a valuable conversation to have - whether or not a component should be able to style more than just top-level nodes inside a slot - but I do think it's a different topic than the title and description here imply.
I wanted to share this codepen that plays around with a few different ways you can use slotted selectors and pseudo elements: https://codepen.io/castastrophee/pen/OJMKeKa
That is not to say that is not a valuable conversation to have - whether or not a component should be able to style more than just top-level nodes inside a slot
Continue the conversation with its full history, that started 5 years ago, and resulted in V1 ::slotted only taking a simple selector,
because ::content and <content> in V0 (allowed for complex selectors, never implemented in FireFox & Safari) did not perform.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/content (deprecated)
( 2015 #331 ) "::slotted" pseudo elements
( 2018 #745 ) ::slotted() should full support complex selector!!
The main take away from 5 years of W3C standards discussions is:
I am not the one to disagree with a Components Lead Developer
Thank you for the links. I am aware of the current spec. Five years is a long time in tech and specs are not immutable. As efficiency is improved, new options may be available. I also was not implying the conversation should be_started_, but pointing out on this thread that it's a different conversation than this one.
FWIW, if complex slotted light dom selectors were permitted, I'd expect the selector in the examples above to be ::slotted(.foo .bar), not ::slotted(.foo) .bar. The latter seems to describe a slotted light dom element ".foo" with a descendent shadow element ".bar". Since that scenario can't seemingly occur, the problem might not be obvious ... but switch to just about any other combinator and you get a scenario that _can_ occur (e.g. ::slotted(.foo) + .bar - "style the shadow .bar when preceded by a slot with an assignee matching .foo").
AFAIK, selectors like this are _also_ not currently permitted, but I'm unsure if that's a matter of deliberate design or not; they still seem to adhere to query-direct-assignees-only.
Edit:
Thinking about this further, I guess + ... also would never match, given the slot itself is "in the way" - the assignee is not a sibling. So perhaps this distinction doesn't matter, at least so long as CSS continues to have no "backwards" selectors.
If you want to track selectors (in this case :host-context) about to be removed from the spec also read:
but I do think it's a different topic than the title and description here imply.
@castastrophe Did you mean the title I chose doesn't match what I proposed in the OP? If so, in my mind I think it matches because it says "combinators postfixed to the ::slotted() selector", and then I am describing what I believe would be intuitive for that to do.
FWIW, if complex slotted light dom selectors were permitted, I'd expect the selector in the examples above to be
::slotted(.foo .bar), not::slotted(.foo) .bar. The latter seems to describe a slotted light dom element ".foo" with a descendent shadow element ".bar".
@bathos That is not intuitive because it is impossible. Why would someone be thinking that, when it doesn't exist?
That's like if I said .foo > .bar should select any .foo elements that have _greater_ amount of text content than the .bar element with largest amount of text. But I'd be making things up at that point.
I think a better interpretation is this:
::slotted(.foo .bar) represents an element in the light tree that matches .foo .bar in the light tree (it is a descendant of a .foo element anywhere in that light tree). I think that's intuitive.::slotted(.foo) .bar represents the .bar element that happens to be a descendant of a slotted .foo element. This is also intuitive.I guess
+ ...also would never match,
If we think intuitively about this, then: a selector like ::slotted(.foo) + .bar would style a .bar element that happens to be the "adjacent sibling" of a .foo element where the .foo element is a slotted element. This is intuitive.
Note that the .bar element could very well be distributed to an entirely different slot (but still have the styling specified for that selector). That makes intuitive sense and could be _totally useful_.
@Danny-Engelman, @hayatoito's comment you screenshotted shows no performance metrics. I am not a browser developer, but I very much doubt (I could be wrong) that ::slotted(.foo) .bar could really be so slow that it matters for the vast majority of use cases.
I wrote a comment about that at https://github.com/w3c/webcomponents/issues/745#issuecomment-668922849.
What I mean is, there's plenty of ways to make _really slow_ selectors in regular DOM, without any shadow DOM even existing. We should not throw out an idea based on a single thought that said it would be slow without any viable data.
What if I said "multi-pass WebGL rendering is slower than single-pass rendering, so we should throw out multi-pass APIs".
But in fact, multi-pass rendering can be _very useful when the performance implications fit within given constraints_.
_It would be great to give web developers useful selectors, and then explain to them that they should avoid re-running these selectors repeatedly; but that it is fine if the cost fits within performance requirements for the application._
I feel that we're prematurely optimizing here. However I know that the web APIs can't be reversed (though I've been imagining how to do that without breaking the web).
Every single DOM API that exists can technically be slooow if we examine it within the context of a particular use case that happens to be the worst use case where we'd never want to perform the given action in the way it is performed.
::slotted(.foo) .bar were supported) unless the use case still fits withing some given performance requirements:connectedCallback() {
requestAnimationFrame(function loop(t) {
this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz').style.transform = `rotateY(${10 * Math.sin(t * 0.001)}deg)`
requestAnimationFrame(loop)
})
}
connectedCallback() {
const el = this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz')
requestAnimationFrame(function loop(t) {
// does not re-run the selector every time:
el.style.transform = `rotateY(${10 * Math.sin(t * 0.001)}deg)`
requestAnimationFrame(loop)
})
}
@hayatoito (and @emilio from the other thread) Can you please expand on the performance issues, and provide useful metrics that we can reference here?
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 fwiw. 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.
@emilio wpuld something like I described in #883 work better, which is essentially inverting part?
The following may be much better (if the selector were supported) for a use case in which the functionality works as intended:
connectedCallback() {
const el = this.shadowRoot.querySelector('::slotted(.foo) .bar + .baz')requestAnimationFrame(function loop(t) {
// does not re-run the selector every time:
el.style.transform =rotateY(${10 * Math.sin(t * 0.001)}deg)
requestAnimationFrame(loop)
})
}
I'm just not sure that referencing slotted content inside the ShadowRoot is possible or makes logical sense if we consider the main benefit of web components: scope. The ShadowRoot does not know what it's slotted content looks like, only that it has slots. The slots point to the light DOM. If you want to capture the light DOM on a component, you use: this.querySelector(".foo"). If you want to make sure .foo is assigned to a slot, you query for: this.querySelector("[slot].foo") or even this.querySelector("[slot=bar].foo") if you want to make sure it's in a specific slot. There is no need for the ::slotted selector inside the querySelector. ::slotted in CSS was a means for the component to be able to influence the light DOM styles directly nested inside the slot (and only directly, top-level items) and it does it very weakly. Most light DOM styles will overwrite anything the component tries to apply to a ::slotted style unless that component uses !important to beat it.
tldr; Scope is the primary discussion here imo. A component is tightly scoped to see it's own template and it's top-level nodes assigned to a slot, no deeper and no higher. That scope is a powerful tool that can be leveraged.
@calebdwilliams I think for the dialog usecase named slots are a reasonable solution. You then could do slot[name="cancel"]::slotted(button) { whatever } or what not.
Introducing a particular part-like attribute definitely mitigates the "now all elements need to look at all their ancestors for slots", for sure (at the cost of adding one more part-like attribute, which is also a bit of an annoyance because it involves one extra branch in all attribute mutations, but probably not a huge issue).
That being said, allowing arbitrary access to the slotted DOM is a bit fishy. That way you start depending on the shape of your slotted DOM tree and that reintroduces the same issue that you're trying to solve with shadow DOM in the first place, which is making an isolated, reusable component. I think that's the point that @catastrophe is making, which I agree with.
@emilio, yeah, I wrestled with that but I keep coming back to the idea that the slotted content is not (or should not be) necessarily required. A dialog without a cancel button is potentially fine.
Granted that might not be how people use the feature.
That's fine? You can have an empty slot. Anyway that's a bit off-topic anyhow.
That being said, allowing arbitrary access to the slotted DOM is a bit fishy. That way you start depending on the shape of your slotted DOM tree and that reintroduces the same issue that you're trying to solve with shadow DOM in the first place, which is making an isolated, reusable component. I think that's the point that @catastrophe is making, which I agree with.
@emilio In some ways, I feel you both on that sentiment.
But there are other possibilities too. The idea is that we allow custom element authors to be more inventive by giving them flexibility.
For example, a custom element author may describe certain usage requirements in the component documentation, and it could require a user to nest elements like follows, where the foo- prefix denotes the elements from the component author's foo- lib:
<foo-interesting-layout>
<div slot="left">
... any other stuff ...
<foo-close></foo-close>
... any other stuff ...
</div>
<div slot="center">
... any other stuff ...
<foo-open-left></foo-open-left>
... any other stuff ...
<foo-open-right></foo-open-right>
... any other stuff ...
</div>
<div slot="right">
... any other stuff ...
<foo-close></foo-close>
... any other stuff ...
</div>
</foo-interesting-layout>
Now, the foo- lib author needs to style the slotted elements, as well as the nested foo- elements are certain way for this layout (f.e. positioning, or something). The component author could use ::slotted(), ::slotted() foo-close, ::slotted() foo-open-left, and ::slotted() foo-open-right within the <foo-interesting-layout> element's shadow root in order to perform the necessary styling.
In my mind, this sort of nesting is a totally valid thing that a library author could document as a requirement, and therefore should have some easy way to perform the styling.
The most important thing to note is that performing the styling is entirely possible today, the feature I ask for only makes it easier with less code.
The way we can do it today is the library author places a <style> element in the nearest root (be that the Document, or nearest ShadowRoot). That of course is less ideal, but completely doable, and more _error prone_.
If the author was able to use ::slotted() foo-open-left, it would keep the styling _co-located with the components it is meant to accompany without extra complication and maintenance burden_.
As with many features of a language or API, there's wrong ways to do just about anything, but I do believe this feature would give authors easier inventiveness without (for example) having to track root nodes and ensure that they don't have duplicate <style> tags.
If the foo-interesting-layout author has selectors like ::slotted() foo-whatever, they can write simpler code, and rely on web APIs like adoptedStyleSheets to handle de-duplication of stylesheets.
@emilio @catastrophe If the custom element author relies on certain slotted DOM structure without documenting that, of course that's bad. It isn't to say a custom element author can't make good documentation to describe what an end user should do. Secondly, without any documentation, the end user will have a hard time _guessing_ what the structure should be anyway, so they probably wouldn't even bother to use that custom element. People generally don't like to guess how an API works.
So that point, though fully valid, doesn't have as high of a significance (in my humble opinion) as the proposed feature does, in that the proposed feature would allow CE authors to achieve things more easily. (Those things should be documented for end users.)
@calebdwilliams I think for the dialog usecase named slots are a reasonable solution. You then could do
slot[name="cancel"]::slotted(button) { whatever }or what not.
@emilio For that particular use case, where the slotted thing is a simple <button> and nothing more, that would work. But it is easy to imagine use cases where an end user is asked to supply DOM trees with certain structures, or even just certain elements anywhere inside the tree; it's a valid use case. For example, even with builtins we have <table>, <thead>, <tr>, <td>, etc.
A CE author could be fairly strict with the requirements, f.e. the author would document strict requirements and be using selectors like ::slotted(foo) > bar, or the well-documented requirements could be less strict while the author would use selectors like ::slotted() bar.
One feature in particular that relies on lose structure is CSS transforms. CSS transform causes absolutely-positioned transformed elements to escape from their DOM hierarchy layout, and they enter into their own 3D layout within the nearest position:relative element. In order to perform the proper styling with these transforms in a slotted tree, we would have less-strict requirements (f.e. ::slotted() bar instead of ::slotted() > bar).
_Imagine the 3D possibilities:_ imagine how a custom element wrapping a DOM tree, relying on selectors post-fixed to ::slotted could make certain elements break out into 3D space, and the only thing the end user has to do is wrap the tree with the custom element, then apply names (classes or attributes, or something) to elements that will break out into 3D space. The wrapper custom element enforces a scope where the 3D effects are applied, and does other things under the hood like use a canvas for WebGL effects added to the elements.
(I'm working on this at http://lume.io, but I have a lot left to do... F.e. here's WebGL-enhanced <button> elements, but it is not using the approach I just described, which is something I've been thinking about adding to make it easier to add 3D to existing DOM whereas currently 3D scenes need to be defined from scratch.)
There's many possibilities. What we've just imagined is doable today, but ::slotted() foo could make it simply easier.
I think, at least slotted parts should be supported by minimum:
@OnurGumus From reading that, I'm not sure what is being proposed there or how that's an alternative to the OP. Could you provide an example?
Is it to say that the light DOM author could specify parts, then the Custom Element author (or ShadowDOM author) could style those ::parts even if they are nested any level deep inside of a slotted node?
If that's what is meant (the light DOM author specifies stylable ::parts), I see how that can satisfy some of the scenerios discussed above, but with some limitations. Namely, the above would allow a custom element author to style _any_ elements in the light DOM without the light DOM author _having to explicitly label all of them with part_.
For example, ::slotted() * can style all of the elements inside a slotted node, but to do this with ::part the light DOM author would need to go and apply the part to all of the elements (or worse, the custom element author would need to traverse the light DOM and add the parts and hence mess with the light DOM author's interface in a possibly unexpected way).
@trusktr What we know is slotted is limited to the "public surface" of the component for performance reasons. OP argues that we shouldn't have that limitation whereas probably some people would reject that.
What I argue is as a user I should be at least use ::slotted(::part Foo ...) since parts are exposed public parts of a component. But even that is not allowed.
parts are exposed public parts of a component
The to make sure we're on the same page, the OP is not talking about exposing parts of a component to the user, the OP is talking about the component being able to more fully work with the tree that the user passed into the component as a distributed/slotted tree; to be able to _more easily_ access that tree in style context (we can already access the, and style all of its elements).
In React, for example, this is easy to do. The component author simply iterates over the array this.props.children and can work with anything as needed: read data, change styling, replace props, etc, before finally passing it into the component's internal ("shadow") tree, all without affecting the component user's outside interface. It's like a map function: it takes a set of children as input (with their descendants) and can map it to whatever the component author desires.
This easy flexibility is what the OP is asking for, but for Web Components, and in this case the ::slotted() .foo selector with combinators would add that flexibility in the context of CSS styling.
The main thing to note, is that _Web Component authors can already access all light DOM and style the elements any way they wish._ The OP only aims to make it easy to implement for WC authors.
<foo-bar>.connectedCallback, for the first instance of <foo-bar> in a document or shadow root, the CE author attaches a <style> element to the document or shadow root where the <foo-bar> element lives.<style> element contains foo-bar .foo {...} to style any .foo elements that are descendants of <foo-bar> elements.foo elements may be descendants of elements that will be slotted into <foo-bar> (effectively this achieves the equivalent ::slotted() .foo that the OP asks for).disconnectedCallback, when there is only one instance of <foo-bar> left (achieved with ref counting) in the document or shadow root where the aforementioned <style> element is placed, the <style> element is removed.So you see, what the OP asks for is totally possible today. The OP merely asks for a concise and simple syntax option.
What a disappointing arbitrary limitation. Let the browser-makers solve performance issues and write the spec to be ideal. WEAK!
I want complete control over the styling of slots and all their children. I wish there was a flag we could flip so that slots would ignore non-shadow styling. A slot is really just a really ugly argument to a function. If you pass something into my function (custom element) I should be able to do what ever I want to that input and all its children too.
The whole goal, for me, is to have every aspect of my web component contained in one file and I don't like having to fool with document level CSS to style children of shadow slotted element. I can still do what I'm wanting with hacks and tricks, but allowing shadow level CSS to style children of slotted elements would make the whole experience much nicer. What @trusktr has been saying is ideal to me.
Make the spec ideal and let the browsers-makers earn their money. They're smart enough to overcome the performance issues, so don't be so easy on them in the spec. This stuff is for the whole world. Let's impress the aliens when they arrive. Their web standards already do this!
There is no fool with document level CSS issue, the only limitation is:
You have to wrap your component _A_ (with shadowDOM and slots) in another component _B_ with shadowDOM.
You can then do all the styling you want in _A_ lightDOM.
Then the browser-makers can focus on creating something that _really_ impresses aliens...
Maybe Apple can do Customized Built-In Elements
This is becoming an "I think Array index should start at 1" topic 馃枛
Since Lonnie Best challenged me, here is my workaround to style all slotted content without using ::slotted
Code at: https://jsfiddle.net/CustomElementsExamples/Lhcsd2m5/

Code at: https://jsfiddle.net/CustomElementsExamples/Lhcsd2m5/
````javascript
customElements.define('to-do', class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure we can access (light)DOM here
let template = document.getElementById(this.nodeName).content.cloneNode(true);
let div = this.attachShadow({ // create TO-DO with shadowDOM and DIV in it
mode: 'open'
}).appendChild(document.createElement("div"));
div.attachShadow({ // attach shadowDOM to DIV container
mode: 'open'
}).append(template); /* add Template content to DIV shadowRoot */
div.append( // move to DIV lightDOM:
div.shadowRoot.querySelector('#styleslots'), // style originally from Template
...this.children // all original to-do lightDOM (this required the setTimeout!)
);
})
}
});
````
Most helpful comment
I'm just not sure that referencing slotted content inside the ShadowRoot is possible or makes logical sense if we consider the main benefit of web components: scope. The ShadowRoot does not know what it's slotted content looks like, only that it has slots. The slots point to the light DOM. If you want to capture the light DOM on a component, you use:
this.querySelector(".foo"). If you want to make sure.foois assigned to a slot, you query for:this.querySelector("[slot].foo")or eventhis.querySelector("[slot=bar].foo")if you want to make sure it's in a specific slot. There is no need for the::slottedselector inside the querySelector.::slottedin CSS was a means for the component to be able to influence the light DOM styles directly nested inside the slot (and only directly, top-level items) and it does it very weakly. Most light DOM styles will overwrite anything the component tries to apply to a::slottedstyle unless that component uses!importantto beat it.tldr; Scope is the primary discussion here imo. A component is tightly scoped to see it's own template and it's top-level nodes assigned to a slot, no deeper and no higher. That scope is a powerful tool that can be leveraged.