CSS not working properly on Custom HTML Elements
Asked Answered
I

3

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.

Is answered 27/12, 2021 at 2:46 Comment(1)
For what it's worth, you have some invalid HTML. Make sure to quote your attribute values, no-1, and no-2.Petulance
C
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>
  `;
)
Crim answered 27/12, 2021 at 3:34 Comment(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)?Is
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.Crim
P
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

Pasteurizer answered 27/12, 2021 at 9:54 Comment(1)
Agreed, shadow DOM is not needed!Onder
P
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.)

Petulance answered 27/12, 2021 at 3:4 Comment(0)
C
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>
  `;
)
Crim answered 27/12, 2021 at 3:34 Comment(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)?Is
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.Crim

© 2022 - 2024 — McMap. All rights reserved.