CSS: Can you use `:has` within `:host()` selector?

137 Views Asked by At

In the below example, I am trying to style the **host** depending on whether it has a slotted element that has the empty attribute. If it does have such an element, then I wish to add a lime green border:

class Component extends HTMLElement {
    constructor() {
    super().attachShadow({mode:'open'});
    const template = document.getElementById("TEMPLATE");    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
window.customElements.define('wc-foo', Component);
<template id="TEMPLATE">
  <style>
      :host(:has(::slotted([empty]))) {
         border: 2px solid lime;
      }
  </style>
  <div>Custom web component should have a lime border
  </div>
  <slot></slot>
</template>

<wc-foo>
  <div empty>"Empty Div"</div>
</wc-foo>

However this does not work, and I am not sure why. Guessing probably because the :host() selector has to be a simple selector. Is there any other way of achieving it?

PS: This question is not a dup of How to use ":host" (or ":host()") with ":has()" cause that is about selecting the host's children, whereas I am trying to select the host depending on its children.

1

There are 1 best solutions below

0
On

As of writing this answer, :host(:has(...)) selecting the light DOM is only implemented in Safari.

So the following example currently only works in Safari:

class WcFoo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
    <style>
      :host {
        display: block;
        margin: 1em;
      }
      :host(:has([empty])) {
        border: 4px solid lime;
      }
    </style>
     <div>Custom web component should have a lime border</div>
    <slot></slot>
`;
  } 
}

customElements.define('wc-foo', WcFoo);
<h3>Preview this example using Safari</h3>

<wc-foo>
  <div empty>"Empty Div"</div>
</wc-foo>

<wc-foo>
  <div>"No empty attribute"</div>
</wc-foo>

While researching this question, I found some GitHub issues that may provide more context: https://github.com/web-platform-tests/interop/issues/208 . This Safari only answer is also courtesy of Westbrook's GitHub comment: https://github.com/w3c/webcomponents-cg/issues/5#issuecomment-1220786480

What is a solution that works in all browsers today?

You can reflect an attribute onto the host to style the host. To detect that your element has been slotted by the element containing the empty attribute, you can use the slotchange event. In the slotchange event reflect a styling attribute onto the host, and use the selector: :host([empty]) to add the lime border.

class WcFoo extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({
      mode: 'open'
    });
    this.shadowRoot.innerHTML = `
        <style>
          :host {
            display: block;
            margin: 1em;
          }
          :host([empty]) {
            border: 4px solid lime;
          }
        </style>
         <div>Custom web component</div>
        <slot></slot>
    `;
    this.shadowRoot.querySelector('slot').addEventListener('slotchange',
      (evt) => {
        const hasEmpty = evt.target.assignedElements()
          .some(el => el.matches('[empty]'));
        if (hasEmpty) {
          this.setAttribute('empty', '');
        } else {
          this.removeAttribute('empty');
        }
      }
    );
  }
}

customElements.define('wc-foo', WcFoo);
<wc-foo>
  <div empty>"Has lime border"</div>
</wc-foo>

<wc-foo>
  <div>"Will not have border"</div>
</wc-foo>