Let's say I have an app where paragraphs are red by default, but I want to use a custom element (let's call it <blue-stuff>
) to style certain paragraphs as blue and bold. In the snippet below, I attempt to do this with a web component and shadow dom below, relying on the ::slotted
pseudo-element:
customElements.define('blue-stuff', class extends HTMLElement {
constructor() {
super()
.attachShadow({ mode: 'open' })
.appendChild(document.importNode(document.getElementById('blue-template').content, true))
}
})
p {
color: red;
}
<template id="blue-template">
<style>
.blue ::slotted(p) {
color: blue;
font-weight: bold;
}
</style>
<div class="blue">
<slot></slot>
</div>
</template>
<p>Hello I am red!</p>
<blue-stuff>
<p>Hello, I am supposed to be blue and bold!</p>
</blue-stuff>
What surprises me is the paragraph that's supposed to be blue is in fact red, meaning it's prioritizing the simple p
selector over the .blue ::slotted(p)
selector. Normally, specificity would solve this problem but it appears in this case, styling in the "light" dom gets preferred over the shadow dom.
The question: In my example, is it possible for the paragraph in <blue-stuff>
to be styled as blue without the use of !important
?
I've thought of so far:
paragraph
, and select that instead. A bit hard-handed, and not something I'd like to do, as I'm parsing these paragraphs from markdown.p
itself no longer has the rule. While this works for inherited properties like color, it doesn't work for properties like margin.blue-stuff p { ... }
. It works, but now it feels like the component is not self-sufficient.I've worked on this in Firefox.
You are falling in the <slot>
trap
For long answer see: ::slotted CSS selector for nested children in shadowDOM slot
a slotted element is reflected in shadowDOM, it is NOT MOVED to a shadowDOM <slot>
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>must</span> be styled by my container DOM [click me]</p>
</blue-stuff>
::slotted()
does not do anything to Specificity
Font related CSS are inheritable styles, trickling down into shadowDOM,
that is why the font-size
becomes 20px
most CSS does not trickle down into shadowDOM, but CSS-properties do
font-weight is applied because there was no font-weight defined yet
Only way to force your blue in is with !important
inside the ::slotted selector,
see font-family
but you should style from the main DOM:
blue-stuff p {
color:blue
}