0

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:

  • Give each paragraph not in the component a class, call it 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.
  • Apply the red color to whatever wraps the paragraphs, so the p itself no longer has the rule. While this works for inherited properties like color, it doesn't work for properties like margin.
  • Apply the styling in the light DOM, by selecting blue-stuff p { ... }. It works, but now it feels like the component is not self-sufficient.

I've worked on this in Firefox.

Auroratide
  • 2,299
  • 10
  • 15

1 Answers1

2

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>should</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
}
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49