1

I defining my own custom elements and find it handy to add its children elements in a shadow dom, mainly because I can to it in the custom element constructor, while the very same constructor doesn't allow me to add "regular" children.

Custom elements' constructor don't let you add children because a new element is supposed to be empty right after creation.

I wonder is there is a simple way to css and styles in the document can affect the elements in the shadow dom. I want selectors from light dom to reach these elements in shadow dom.

One of my custom elements is an "item". I'll create a lot of them to populate a list. I dislike the idea of repeating the very same style tag in each item's instance' shadow dom, so I'm looking for a place to put this style tag for all my items.

I read many articles about shadow dom and how inner styles don't affect elements out of boundary, but any of them answered my question.

Any thoughts? Thanks!

Alejandro Silvestri
  • 3,706
  • 31
  • 43
  • Possible duplicate of [Share style across web components "of the same type"](https://stackoverflow.com/questions/42645363/share-style-across-web-components-of-the-same-type) – Supersharp Apr 17 '19 at 14:31
  • @Supersharp, I don't think it is a duplicate, my question is very different, sharing a style is not what I was looking for. – Alejandro Silvestri Apr 17 '19 at 21:53
  • Put lightDOM content in SLOTs: https://stackoverflow.com/questions/61626493/slotted-css-selector-for-nested-children-in-shadowdom-slot/61631668#61631668 – Danny '365CSI' Engelman Nov 08 '20 at 11:07

2 Answers2

2

The current version of shadowDOM requires you to place your CSS inside the shadowDOM.

Most CSS is fairly small and only adds a few bytes to a few hundred bytes in each element. Some of the largest I have see add about 2k of CSS into each copy. But this is still very small compared to the data represented in the DOM structure.

There are a few things that will bleed through from the outside, like font information, but not many.

Here are some ways you can affect the from the outside:

1. CSS variables

A CSS Variable allows you to set a variable to a value that is used within the CSS whether in shadowDOM or not.

2. Attributes

Attributes can be captured and migrated into the shadowDOM CSS. I have a few components that use an attribute to define a theme.

3. Properties

Properties can also be taken and applied to internal CSS.

There are other ways in discussion, but those will have to wait until V2.

class MyEL extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    .outer {
      color: var(--myColor, 'black');
    </style>
    <div class="outer">
      <h4>Title</h4>
      <slot></slot>
    </div>`;
  }

  static get observedAttributes() {
    return ['bgcolor'];
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal !== newVal) {
      this.shadowRoot.querySelector('.outer').style.backgroundColor = newVal;
    }
  }

  get border() {
    return this.shadowRoot.querySelector('.outer').style.border;
  }
  set border(value) {
    this.shadowRoot.querySelector('.outer').style.border = value;
  }
}

customElements.define('my-el', MyEL);

setTimeout(() => {
  document.querySelector('my-el').border = '2px dashed blue';
},1000);

const btn = document.getElementById('toggle');
let color = '';
btn.addEventListener('click', () => {
  color = color === '' ? 'white' : '';
  document.querySelector('my-el').style.setProperty('--myColor', color);
});
<my-el bgcolor="red"></my-el>
<hr/>
<button id="toggle">toggle</button>

UPDATED

As stated in my comment below there are a few CSS rules that will penetrate the shadowDOM if they are not overwritten. Things that penetrate are color, background, font, and other things related to those.

This code shows an example of how they penetrate:

customElements.define('css-test', class extends HTMLElement {
  constructor() {
    super();
    var shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `<h1>Header</h1>
    <p>This is a <strong>strong</strong> string</p>
    <p>This is <em>emphasis<em>.`;
  }
});

const styles = document.createElement('style');
styles.textContent = `
* {
  background-color: #900;
  border: 1px solid #9876;
  border-radius: 5px;
  box-sizing: border-box;
  box-shadow: 0 0 4px #0005;
  color: #fff;
  font: 24px Tahoma;
  margin: 20px;
  padding 20px;
  text-align: center;
  text-decoration: #ffd wavy underline;
  text-shadow: 3px 3px 3px #0008;
  transform: rotate(30deg);
}`;

function toggleCss(evt) {
  if(styles.parentElement) {
    styles.remove();
  }
  else {
    document.body.appendChild(styles);
  }
}
const toggleEl = document.getElementById('toggle');
toggleEl.addEventListener('click', toggleCss);
  <css-test></css-test>
  <hr/>
  <button id="toggle">Toggle CSS</button>

When you click on the toggle button you will see that things change color, background color, fonts, etc. But something like rotation only happens to the component and not to the sub elements withing the component. And that is a good thing. Imagine you the user could rotate things within your component... Your entire component would break.

I was looking for a good article that talked about all of the things that penetrate but couldn't find it. I have seen it in the past but forgot to bookmark it. So if someone finds that article please add it into the comments below.

Now here is the same code above with several things overridden so you can see how your component can override what is done on the outside:

customElements.define('css-test', class extends HTMLElement {
  constructor() {
    super();
    var shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `<style>
    :host * {
      background: white;
      color: yellow;
      font-size: 14px;
    }
    </style><h1>Header</h1>
    <p>This is a <strong>strong</strong> string</p>
    <p>This is <em>emphasis<em>.`;
  }
});

const styles = document.createElement('style');
styles.textContent = `
* {
  background-color: #900;
  border: 1px solid #9876;
  border-radius: 5px;
  box-sizing: border-box;
  box-shadow: 0 0 4px #0005;
  color: #fff;
  font: 24px Tahoma;
  margin: 20px;
  padding 20px;
  text-align: center;
  text-decoration: #ffd wavy underline;
  text-shadow: 3px 3px 3px #0008;
  transform: rotate(30deg);
}`;

function toggleCss(evt) {
  if(styles.parentElement) {
    styles.remove();
  }
  else {
    document.body.appendChild(styles);
  }
}
const toggleEl = document.getElementById('toggle');
toggleEl.addEventListener('click', toggleCss);
<css-test></css-test>
  <hr/>
  <button id="toggle">Toggle CSS</button>
Intervalia
  • 10,248
  • 2
  • 30
  • 60
  • What if I don't own the component code, and therefore cannot edit it to interpret css variables, properties, or attributes? Is it really not possible to write light dom css that hacks into Shadow DOM elements? – johncorser Sep 29 '19 at 23:33
  • There are a few CSS properties that will be inherited by ShadowDOM if it is not overwritten. `font`, `color`. See the *UPDATED* section in my examples above. – Intervalia Sep 30 '19 at 14:28
0

First of all, I do not know what you mean with your remark that a custom component should be empty... I do not think that is true, since I cannot find any documentation that states that requirement as such. Furthermore, the following just works fine for me:

customElements.define("my-element", class extends HTMLElement {
  constructor() {
    super();

    // Add a `span` element to the component's light-DOM.
    const span = document.createElement("span");
    span.innerHTML = "Hello, World!";
    this.appendChild(span);

    // Add a `slot` element to the component's shadow-DOM (so that the light-DOM gets rendered).
    this.attachShadow({ mode: "open" }).innerHTML = "<slot></slot>";
  }
});
<my-element></my-element>

If you mean that light-DOM content will not be rendered if it cannot be assigned to a slot element in the shadow-DOM, then you are right, of course.

Anyway, that was just a side node. Back to your question about styling the shadow-DOM from outside the web component...

In addition to @Intervalia's quite thorough answer, you may also take a look at the ::part and ::theme pseudo elements of web components.

Something like this:

customElements.define("my-element", class extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" }).innerHTML = `
      <style>
        * {
          position: relative;
          box-sizing: border-box;
          margin: 0;
          padding: 0;
        }
        :host {
          display: inline-block;
          height: 150px;
          width: 200px;
        }
        #outer {
          height: 100%;
          padding: 20px;
          background: lightblue;
          border: 5px solid blue;
        }
        #inner {
          height: 100%;
          background: lightgreen;
          border: 5px solid green;
        }
      </style>
      <div id="outer" part="foo">
        <div id="inner" part="bar">
        </div>
      </div>
    `;
  }
});
#second {
  width: 300px;
}

#second::part(bar) {
  background: lightcoral;
  border: 10px dotted red;
}
<my-element id="first"></my-element>
<my-element id="second"></my-element>

For a thorough explanation, I would like to refer to:

Note that the ::part pseudo element and the corresponding part attribute currently only work in Chrome and Opera. FireFox will support it in version 69, but first you will have to set layout.css.shadow-parts.enabled to true in its about:config page (which makes me doubt that it will be very useful for regular users using Firefox, so I hope Mozilla will turn it on by default very soon).

Bart Hofland
  • 3,700
  • 1
  • 13
  • 22