Skip to content
Advertisement

What happens to child nodes if there is no “, but a shadow root

Consider this code:

//js
class FooBar extends HTMLElement {
  constructor(){
    super();
  }
}

customElements.define('foo-bar', FooBar);


<!-- html -->
<foo-bar>
  <h1>Test</h1>
</foo-bar>

This will show »Test« within the browser.

If the constructor is changed to:

constructor () {
  super();
  this.shadow = this.attachShadow({ mode: 'open' }) 
}

The »Test« disappears, since there is a shadow root now.

If the constructor is furthermore changed to

constructor () {
  super();
  this.shadow = this.attachShadow({ mode: 'open' });
  this.shadow.appendChild(document.createElement('slot')); 
}

The »Test« appears again, since there is now a default slot for all child Nodes of <foo-bar>

But what happens to the child nodes if there is no <slot /> within the shadow root. They still appear within this.children and its style.display property remains "". So they are within the dom, but not rendered, even thou thr css tells the opposite? What exactly happens here?

Advertisement

Answer

The full detailed explanation is at: ::slotted CSS selector for nested children in shadowDOM slot


<foo-bar>
  <h1>Test</h1>
</foo-bar>

H1 is lightDOM,
“added” to shadowDOM/root <SLOT> the content is reflected to shadowDOM, NOT moved!!!

H1 always remains in lightDOM :

  • invisible (in the page) in lightDOM for elements with shadowDOM/root,

  • visible (in the page) for Custom Elements without shadowDOM/root

  • unless you move it explicitly with appendChild (or any DOM move operation)

You say: So they are within the dom, but not rendered, even thou CSS tells the opposite?

No, they are rendered, just like any normal DOM element. Just not visible any more.

You can test by including a SCRIPT tag in lightDOM.. it will render and execute!


In code snippets below

You reference lightDOM with this.querySelector("span").innerHTML="weird";

But referencing shadowDOM with this.shadowRoot.querySelector("span").innerHTML="weird";

Does not work, because the DIV (with the SPAN inside) is black-boxed in a <SLOT>

<template id="MY-ELEMENT">
  <style>
    :host {
      display: inline-block;
      font-family: Arial;
    }
    ::slotted(div){
      color:blue;
    }
    ::slotted(span){
      color:gold; /* alas, you can style the 'box', not elements inside */
    }
  </style>
  <h3><slot></slot></h3>
</template>
<style>
  span {
    background:lightcoral; /* from global/host CSS, style slotted content lightDOM */
  }
</style>
<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      super().attachShadow({mode: 'open'})  
             .append(document.getElementById(this.nodeName).content.cloneNode(true));
    }
  });
</script>
<my-element>
  <div>Hello <span>Component</span> World!</div>
</my-element>

Check the Component in F12 Dev Tools:

Chrome & Firefox:

The DIV is not in shadowDOM/root, remains invisible in lightDOM
all elements/styles will always reflect to shadowDOM/root

click ‘reveal’ takes you to the lightDOM

So to shadowDOM, slotted content is a black-box of elements & styles;
reflected from lightDOM
that is why ::slotted can only style the box, and not what is inside.

Note: edit that DIV in F12 Console, you will see changes immediately reflect to shadowDOM


SLOTs & lightDOM are LIVE connections

By changing <slot name=...> you can make interactions (think Routes, Tabs, Answers) which previously needed lots more coding (remember those jQuery show/hide days?)

<template id="MY-ELEMENT">
  Custom Element SLOTs are: 
  <slot name=answer></slot>
</template>
<style>
  img { /* style all IMGs in lightDOM */
    max-width: 100vw;
    max-height: 70vh;
  }
</style>
<script>
  customElements.define('my-element', class extends HTMLElement {
    connectedCallback() {
      this.attachShadow({mode: 'open'})
          .append(document.getElementById(this.nodeName).content.cloneNode(true));
      this.onclick = (evt) => {
           const answer = evt.composedPath()[0].innerText; // button label
           this.shadowRoot.querySelector('slot').name = answer;
           this.children[0].slot = answer;//include lightDOM buttons again
      }
    }
  });
</script>
<my-element>
  <span slot=answer><button>Cool</button><button><b>Awesome</b></button><button>Great</button></span>
  <div slot=Cool><img src="https://i.imgur.com/VUOujQT.jpg"></div>
  <span slot=Awesome> <h3>SUPER!</h3></span>
  <div slot=Awesome><img src="https://i.imgur.com/y95Jq5x.jpg"></div>
  <div slot=Great><img src="https://i.imgur.com/gUFZNQH.jpg"></div>
</my-element>

More SLOT related answers can be found with StackOverflow Search: Custom Elements SLOTs

Advertisement