1

I have a web component with a shadow DOM and a default slot.

I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.

I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.

Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).

const styleStr = `
  :host {
    display: block;
    border: 3px dotted red;
  }
  :host(:has(div)) {
    border-color: green;
  }
`;
let css;
try {
  css = new CSSStyleSheet;
  css.replaceSync(styleStr);
} catch(e) { console.error(e) }

customElements.define('x-y', class extends HTMLElement {
   constructor() {
     super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
     this.shadowRoot.append(document.createElement('slot'))
   }
})
<x-y>no div - should have red border</x-y>
<x-y>
  <div>div, should have green border</div>
</x-y>

I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.

Does anyone have definitive knowledge/reference about this, and can point me to some documentation?

connexo
  • 53,704
  • 14
  • 91
  • 128
  • 1
    if I am not mistaking, the :host element is outside the Shadow Tree so your logic won't work since a selector cannot "cross" the shadow to test the presence of some elements – Temani Afif Jan 20 '23 at 10:37
  • That would effectively mean that `:host(:has())` cannot work. Would love to see that mentioned in the spec or documentation somewhere. That is an important limitation for both `:has()` and `:host()`. – connexo Jan 20 '23 at 10:42
  • if my logic is correct then :has() has no meaning inside :host() but it can also be an implementation issue and :host() combined with :has() is still not a thing. – Temani Afif Jan 20 '23 at 10:44
  • So then probably the only way to get this implemented is to make the element that needs this conditional styling a `part` and go with regular DOM CSS like `x-y:has(div)::part(that-part) { ... }`, which is really unfortunate because it not only forces me to expose styling of that element, but also to publish CSS in the regular DOM. – connexo Jan 20 '23 at 10:46
  • In your example a global style ```` also works, because slotted content remains in lightDOM. ``host`` documentation says "This has no effect **outside** a shadowDOM" I agree its confusing – Danny '365CSI' Engelman Jan 20 '23 at 10:50
  • The problem is that the slotted element in this case is also a webcomponent with a slot and shadow DOM. – connexo Jan 20 '23 at 10:51
  • 1
    Then you need a `` – Danny '365CSI' Engelman Jan 20 '23 at 10:52

1 Answers1

0

You want to style slotted content based on an element inside the slot

Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.

If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV

Then creates a <style> element in the container element

Disclaimer: Provided code is a Proof of Concept, not production ready

<my-component>
  Hello Web Component
</my-component>

<!-- <my-component> will add a STYLE element here -->
<my-component>
  <!-- <my-component> will assign a unique ID to the DIV -->
  <div>Web Component with a DIV in the slot</div>
</my-component>

<script>
customElements.define("my-component", class extends HTMLElement {
  constructor() {
    super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
    let slot = this.shadowRoot.querySelector("slot");
    slot.addEventListener("slotchange", (evt) => {
      [...slot.assignedNodes()].forEach(el => {
        if (el.nodeName == "DIV") {
          el.id = "unique" + new Date() / 1;
          // inject a <style> before! <my-component>
          this.before( Object.assign( document.createElement("STYLE"), {
             innerHTML : `#${el.id} { background:lightgreen } `
          }));
        }
      });
    });
  }
})
</script>

PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49