5

I've been trying to make a custom HTML Element by extending the HTMLElement class. I try adding some style to it by linking a CSS file that is in the same directory as my other two files - index.html and custom.css.

Main folder

  • index.html
  • custom.css
  • custom.js

index.html:

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="nofollow" type="text/css" href=''>
</head>
 
    <body>
        <script src="./custom.js"></script>
        <smooth-button text="Smooth button" no-1 = 1 no-2 = 2></smooth-button>
    </body>
 
</html>

custom.css:

smooth-button{
    display: block;
    color: blue;
    background-color: orange;
}

custom.js:

class SmoothButton extends HTMLElement{
 
    constructor(){
        super();
        this.shadow = this.attachShadow({mode: "open"})
    }
 
    connectedCallback(){
        this.render();
    }
 
    render(){
        this.SumOfNo1AndNo2 = null;
        if(this.getAttribute("no-1")!=null && this.getAttribute("no-2")!=null){
            this.SumOfNo1AndNo2 = parseInt(this.getAttribute("no-1")) + 
            parseInt(this.getAttribute("no-2"));
        }
        else{
            console.log("Invalid attribute.")
        }
        this.shadow.innerHTML = `<button>` + this.getAttribute("text") + " " + this.SumOfNo1AndNo2   
        + "</button>"
    }
 
}
 
customElements.define("smooth-button", SmoothButton);  

With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of. How can I apply the styles separately to each of its elements (just a <button> for now) with an external CSS file? I'm using external CSS because it's somehow better as I read it here.

1
  • 1
    For what it's worth, you have some invalid HTML. Make sure to quote your attribute values, no-1, and no-2.
    – Brad
    Commented Dec 27, 2021 at 2:50

3 Answers 3

7

In addition to the answers from Brad and Emiel,

  • (Brad) Bluntly add a <style> element inside shadowDOM
  • (Emiel) use cascading CSS properties
  • There are more options to style shadowDOM:

Learn about Inheritable Styles

use shadow parts

<style>
  ::part(smoothButton){
    display: block;
    color: blue;
    background-color: orange;
  }
</style>

<smooth-button></smooth-button>
<smooth-button></smooth-button>

<script>
  customElements.define("smooth-button", class extends HTMLElement {
    constructor(){
      super()
        .attachShadow({mode:"open"})
        .innerHTML = `<button part="smoothButton">LABEL</button>`;
    }
  });
</script>

But...

The first question you should ask yourself:

Do I really need shadowDOM?

If you don't want its encapsulating behavior, then do not use shadowDOM

<style>
  .smoothButton{
    display: block;
    color: blue;
    background-color: orange;
  }
</style>

<smooth-button></smooth-button>
<smooth-button></smooth-button>

<script>
  customElements.define("smooth-button", class extends HTMLElement {
    connectedCallback(){
      this.innerHTML = `<button class="smoothButton">LABEL</button>`;
    }
  });
</script>

shadowDOM <slot>

Another alternative is to use shadowDOM <slot> elements, because they are styled by its container element

<style>
  .smoothButton{
    display: block;
    color: blue;
    background-color: orange;
  }
</style>

<smooth-button><button class="smoothButton">LABEL</button></smooth-button>
<smooth-button><button class="smoothButton">LABEL</button></smooth-button>

<script>
  customElements.define("smooth-button", class extends HTMLElement {
    constructor(){
      super()
        .attachShadow({mode:"open"})
        .innerHTML = `<slot></slot>`;
    }
  });
</script>

When you go down the <slot> rabbithole, be sure to read the (very long) post:
::slotted CSS selector for nested children in shadowDOM slot

1
  • Agreed, shadow DOM is not needed!
    – Kokodoko
    Commented Jun 9 at 9:13
4

Additionally to Brad's answer. One of the ways you can apply styles from the Light DOM to the Shadow DOM is with CSS Variables.

smooth-button{
  display: block;

  --button-color: blue;
  --button-background-color: orange;
}
render() {
  this.shadow.innerHTML = `
    <style>
      button {
        color: var(--button-color);
        background-color: var(--button-background-color);
      }
    </style>

    <button>
      ${this.getAttribute("text")} ${this.SumOfNo1AndNo2}   
    </button>
  `;
)
2
  • Thanks a lot for the answer! Just asking - is there any way to cache files (a .txt file?) using code (not dependent on the browser)? Is there any way to get the cached files (using code)? Commented Dec 30, 2021 at 9:10
  • 1
    You're welcome. For new questions, please open up a new question in SO, but for now: you can use the Cache API in combination with the Service Worker API to cache any files requested by the client. Commented Dec 30, 2021 at 10:22
4

With this, I get a button as expected, with the text, but the style is applied to the element as a whole and not to the elements it's made of.

This is actually how the custom element is supposed to work. You can't apply styles to the shadow DOM from the outer document. If you could, you'd have a high likelihood of breaking the custom element styling through external modification.

All is not lost however! The reason the button is a different color from its background is due to the user agent stylesheet. You can actually set some CSS to tell the background to inherit the parent background color. Try adding this to your custom element:

const style = document.createElement('style');
style.textContent = `
  button {
    background: inherit;
  }
`;
this.shadow.append(style);

JSFiddle: https://jsfiddle.net/5t2m3bku/

(Also note that it's not really a great idea to interpolate/concatenate text directly into HTML. That text gets interpreted as HTML, which can lead to invalid HTML if reserved characters are used, and even potential XSS vulnerabilities. You might modify that line where you set innerHTML to set the text, or switch to a template engine.)

Not the answer you're looking for? Browse other questions tagged or ask your own question.