How do I style the last slotted element in a web component
For long answer on ::slotted see: ::slotted CSS selector for nested children in shadowDOM slot
From the docs: https://developer.mozilla.org/en-US/docs/Web/CSS/::slotted
::slotted( <compound-selector-list> )
The pseudo selector goes inside the brackets: ::slotted(*:last-child)
Note: :slotted(...)
takes a simple selector
See (very) long read: ::slotted CSS selector for nested children in shadowDOM slot
customElements.define('my-table', class extends HTMLElement {
constructor() {
let template = (name) => document.getElementById(name)
.content.cloneNode(true);
super()
.attachShadow({ mode: 'open' })
.append( template(this.nodeName) );
}
})
<template id="MY-TABLE">
<style>
:host { display: flex; padding:1em }
::slotted(*:first-child) { background: green }
::slotted(*:last-child) { background: yellow; flex:1 }
::slotted(*:first-of-type) { border: 2px solid red }
::slotted(*:last-of-type) { border: 2px dashed red }
</style>
<slot name="column"></slot>
</template>
<my-table>
<div slot="column">Alpha</div>
<div slot="column">Bravo</div>
<div slot="column">Charlie</div>
</my-table>
<my-table>
<div slot="column">Delta</div>
<div slot="column">Echo</div>
</my-table>
How to style slotted parts from the parent downwards
You had multiple issues
- Typos: You are mixing
title
andtest
for your::part(x)
references - ::slotted is a very simple selector, so you can discard all those tries
- (per above link) Slotted content remains in lightDOM; so your elements in lightDOM:
<my-grid>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
<my-post></my-post>
</my-grid>
must be styled from its container.... in this case the main document DOM
So all global style required is:
my-post::part(title) {
background: red;
}
You can not do this in <my-grid>
because <my-post>
is not inside <my-grid>
lightDOM<my-grid>
can not style its slotted content (only the 'outer' skin with ::slotted
)
I added extra styling, slots and a nested <my-post>
element to make things clear
<script>
class GridElements extends HTMLElement {
constructor() { super().attachShadow({mode: 'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true)) }}
customElements.define('my-grid', class extends GridElements {});
customElements.define('my-post', class extends GridElements {});
</script>
<template id="MY-GRID">
<style>
:host{display:grid;grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) }
::slotted(my-post) { background: green }
</style>
<slot></slot>
</template>
<style id="GLOBAL_STYLE!!!">
body { font: 14px Arial; color: blue }
my-post::part(title) { background: red }
my-grid > my-post::part(title) { color: gold }
my-post > my-post::part(title) { background:lightcoral }
</style>
<template id="MY-POST">
<h1 part="title"><slot name="title">[Title]</slot></h1>
<slot>[post body]</slot>
</template>
<my-grid>
<my-post><span slot="title">One</span></my-post>
<my-post>Two</my-post>
<my-post></my-post>
<my-post>
<my-post><span slot="title">SUB</span></my-post>
</my-post>
</my-grid>
Styling descendent element in slotted element
No, you can only select top-level nodes with ::slotted()
.
The selector inside ::slotted() can only be a compound selector, so div p
is not a valid one.
According to Hayato Ito :
The reason of this restriction is to make a selector style-engine friendly, in terms of performance.
See the styling example in the Shadow Dom v1 presentation.
Styling default slot in webcomponents
Found a little workaround until someone finds a better way. We can use slotchange event to make sure whether any items attached to the slot or not. In this way.
HTML
<slot (slotchange)="onSlotChanged($event)"></slot>
JS/TS
onSlotChanged($event) {
const slotHasData = $event.target.assignedNodes().length > 0;
// Is event.target.assignedNodes().length return more than 0, it has nu of items attached to the slot
}
Applying selection style to slotted elements?
You can't, because slotted content is NOT moved to shadowDOM, it remains in ligthDOM.
You style slotted content in ligthDOM (in this case the main DOM)
For very detailed answer see: ::slotted CSS selector for nested children in shadowDOM slot
I added extra CSS to show:
Using variables (that penetrate shadowDOM) to declare a colors ones
Using a
#selectable
DIV wrapper selects both custom elements
See for yourself what thex-widget ::selection
selector would do
Select all text:
<div id=selectable>
<x-widget>CONTENT1</x-widget>
<x-widget><div>CONTENT2</div></x-widget>
</div>
<style>
body {
--selectionBackground: green; --selectionColor: white;
font-size: 2em;
}
#selectable ::selection {
background: var(--selectionBackground); color: var(--selectionColor);
font-weight: bold;
}
</style>
<script>
window.customElements.define('x-widget', class extends HTMLElement {
constructor() {
const template = document.createElement('template');
template.innerHTML = `<div>TEMPLATE</div><div><slot></slot></div>`;
const style = document.createElement('style');
style.textContent = `
::selection { /* colors only template text, not slot content */
background: var(--selectionBackground);
color: var(--selectionColor);
}
::slotted(*) { /* selectors select HTMLElements! */
color: red; /* CONTENT1 is TEXT, NOT an HTMLElement! */
}`;
super().attachShadow({mode: 'open'})
.append(style, template.content.cloneNode(true));
}});
</script>
Why styles don't apply to all slot elements in a custom web component?
Change ul li
to ::slotted(li)
.
MDN got example of use-case close to what you trying to do.
Snippet:
const template = document.createElement('template');
template.innerHTML = `
<style>
ul {
list-style-type: none;
padding: 2.5rem 0;
margin-block-start: 0;
margin-block-end: 0;
}
::slotted(li) {
background-image: url("https://www.nextiva.com/assets/svg/bullet.svg");
background-position: 0 0.725rem;
background-repeat: no-repeat;
padding-left: 1.125rem;
margin: 2rem 0;
font-size: 1.25rem;
line-height: 2rem;
}
ul li:first-child, ul li:last-child {
margin: 0;
}
</style>
<ul>
<li><slot name="item"></slot></li>
</ul>
`;
class CustomBulletList extends HTMLElement {
constructor() {
super();
this.showInfo = true;
this.attachShadow({
mode: 'open'
});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define('custom-bullet-list', CustomBulletList);
<custom-bullet-list>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</custom-bullet-list>
::slotted CSS selector for nested children in shadowDOM slot
styling ::slotted elements in shadowDOM
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 selectorsInheritable 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
- https://github.com/whatwg/html/issues/6051#issuecomment-816971072
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:
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>
Prioritizing slotted styling over global styling
You are falling in the <slot>
trap
For long answer see: ::slotted CSS selector for nested children in shadowDOM slot
A. a slotted element is reflected in shadowDOM, it is NOT MOVED to a shadowDOM
<slot>
B. slotted content is styled from the container the (hidden) lightDOM element is defined in
In your case that is the main DOM
customElements.define('blue-stuff', class extends HTMLElement {
constructor() {
super()
.attachShadow({
mode: 'open'
})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
this.onclick = () => BLUE.disabled = !BLUE.disabled;
}
})
p {
color: red;
font-size: 20px;
font-family: Arial;
}
span {
background:gold;
}
<template id="BLUE-STUFF">
<style>
::slotted(p) {
color : blue; /* not applied because inheritable color:red */
font-weight: bold; /* applied, no inherited setting */
cursor: pointer; /* applied, no inherited setting */
font-family: Georgia !important; /* be a crap Designer, force it */
}
::slotted(span){
background:black !important;
/* won't work, ::slotted can only style lightDOM 'skin' (p) */
}
</style>
<slot></slot>
</template>
<style id=BLUE onload="this.disabled=true">/* click to toggle stylesheet */
blue-stuff p{
color:blue;
font-weight: normal;
font-family: Arial !important; /* not applied, fire that 'designer'! */
}
</style>
<blue-stuff>
<p>I <span>should</span> be styled by my container DOM [click me]</p>
</blue-stuff>
Related Topics
Wp_Enqueue_Style and Rel Other Than Stylesheet
Ie8/9 - Maximum Bytes for CSS File
CSS3 Display:Table, Overflow-Y:Scroll Doesn't Work
Absolutely Positioned Flexbox Doesn't Expand to Fit Contents
CSS: :Before: :After Pseudo-Element of Class Not Working
How to Style a Text Input to Fill The Width of It's Parent
Sass Syntax Highlighting in Visual Studio
Angular 6 - How to Apply External CSS Stylesheet (Leaflet) at Component Level
What Do These "\E6##" Characters Mean
Is The CSS3 Transform Translate Percentage Values Relative to Its Width And/Or Height
CSS Argument for "If First Child Is"
Font Size. What How to Be Sure Of
Vertical Line with Dots in Ends and Between
How to Make Entire Div Change Color on Hover Using CSS
Absolute Element Inheriting Relative Parent Div's Width