Search code examples
cssweb-component

Prioritizing slotted styling over global styling


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.


Solution

  • 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
    }