What Is the ::Content/::Slotted Pseudo-Element and How Does It Work

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>

::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

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>

Web Components - why content was replaced with slot

The editor of Shadow DOM specification is here.

It would be difficult to summarize the reason here, however, the following discussions and the minutes might be helpful to understand the history and the reason:

  • https://lists.w3.org/Archives/Public/public-webapps/2015AprJun/thread.html#msg184
  • https://www.w3.org/wiki/Webapps/WebComponentsApril2015Meeting (See "F. Slots Proposal")
  • https://lists.w3.org/Archives/Public/public-webapps/2015AprJun/thread.html#msg649

Yeah, Google opposed to this change at first, but we had no other choice to make Shadow DOM forward. :)

I have rewritten Shadow DOM specification as "v1" so that it will be the standard on which all browser vendors could agree. That's the primary reason.

We all are aware that the API change is painful for Web developers, but I believed that "Native Shadow DOM in every browsers" is much important for the Web platform in the long term.

See also here for other changes between v0 and v1:

what is `::content` in a css?

I'm having trouble finding official documentation on this pseudo-element selector, but it's a selector for Shadow DOM elements. See the following excerpts from the AdBlock Plus Chrome extensions source code:

include.preload.js convertSelectorsForShadowDOM

function convertSelectorsForShadowDOM(selectors)
{
var result = [];
var prefix = "::content ";

...

include.preload.js call to convertSelectorsForShadowDOM

    // If using shadow DOM, we have to add the ::content pseudo-element
// before each selector, in order to match elements within the
// insertion point.
if (shadow)
selectors = convertSelectorsForShadowDOM(selectors);

use :content pseudo-element to change non-first thead th element content

You might be able to use the :nth-child pseudo-element.

http://css-tricks.com/5452-how-nth-child-works/

You can target only specific rows with :nth-child(), or :nth-of-type(). I'm not certain that :nth-child can handle a range though, so you may have to chain you css rather annoyingly like:

:nth-child(3), :nth-child(4), :nth-child(eleventy!!1!) {properties}

More:
http://www.quirksmode.org/css/nthchild.html

Now, of course - if you were using Sass or Compass - this chaining would be simple. Check both items out.

Styling `::slotted()` elements from the static styles property

Seems to be a syntax problem in my example. I was using an style array.

This is working fine

static get styles(): CSSResultArray {
return [
css`
:host {
/* styles */
}

::slotted(span) {
/* styles */
}
`,
css`
:host([data-type="primary"]) button {
/* styles */
}
`
];
}



Related Topics



Leave a reply



Submit