Share Style Across Web Components "Of The Same Type"

Share style across web components of the same type

Does it have any performance implications...?

Yes, it depends on how many instances, and on the CSS engine implemented in the browser. You'll have to test every use case and take in account speed versus memory consumption.

Is there a way to share the style node across multiple instances of the same web component?

Yes, you can use @import url like in this SO question. Or you can choose to not use Shadow DOM and use global CSS style only.

2019 update

As Harshal Patil suggested, since Chrome 73 and Opera 60 it is possible for multiple Shadow DOM to adopt the same stylesheet. This way an update in the stylesheet will be applied to all the web components.

let css = new CSSStyleSheetcss.replaceSync( `div { color: red }` )
customElements.define( 'web-comp', class extends HTMLElement { constructor() { super() let shadow = this.attachShadow( { mode: 'open' } ) shadow.innerHTML = `<div><slot></slot></div>` shadow.adoptedStyleSheets = [ css ] }} )color.oninput = () => css.replaceSync( `div { color: ${color.value} }` )
<web-comp>Hello</web-comp><web-comp>World</web-comp><input value=red id=color>

Style nested web component where child can be same type as, but needs different styles to, parent

Took some time to fully understand what you want (and I could be wrong)

  • You want to specify the margin-top for all CHILDREN (except the first child)

    with: <vertical-layout childmargin="2em">
  • For nested <vertical-layout> the element should have the margin-top of its PARENT container

Problem with your: <vertical-layout style="--spacing-size: 2em">, is that the 2em is set on the <vertical-layout> itself (and all its children)

You want it applied to children only

You can't do that with CSS in shadowDOM; because that doesn't style slotted content.

See: ::slotted CSS selector for nested children in shadowDOM slot


I have changed your HTML and attributes to reflect the margins you want:

(px notation for better comprehension)

    0px <vertical-layout id="Level1" childmargin="15px">
15px <div>child1-1</div>
15px <div>child1-2</div>
15px <div>child1-3</div>
15px <vertical-layout id="Level2" childmargin="10px">
0px <div>child2-1</div>
10px <div>child2-2</div>
10px <vertical-layout id="Level3" childmargin="5px">
5px <div>child3-1</div>
5px <div>child3-2</div>
5px <div>child3-3</div>
</vertical-layout>
10px <div>child2-3</div>
</vertical-layout>
15px <div>child1-4</div>
15px <div>child1-5</div>
</vertical-layout>

CSS can not read that childmargin value; so JS is required to apply that value to childelements

As you also don't want to style the first-child...

The code for the connectedCallback is:

    connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.forEach(child=>child.style.setProperty("--childmargin", margin));
});
}

Notes

  • * is a bit brutal.. you might want to use a more specific selector if you have loads of child elements; maybe:
[...this.children].forEach((child,idx)=>{
if(idx) ....
};
  • You are looping all children; could also set the style direct here.. no need for CSS then

  • The setTimeoutis required because all child have not been parsed yet when the connectedCallback fires.

Because all your <vertical-layout> are in GLOBAL DOM (and get refelected to <slot> elements)

You style everything in GLOBAL CSS:

  vertical-layout > *:not(:first-child)  {
margin-top: var(--childmargin);
}

Then all Web Component code required is:

customElements.define("vertical-layout", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = "<style>:host{display:flex;flex-direction:column}</style><slot></slot>";
}
connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.forEach(child=>child.style.setProperty("--childmargin", margin));
});
}
});

Sample Image

<vertical-layout id="Level1" childmargin="15px">
<div>child1-1</div>
<div>child1-2</div>
<div>child1-3</div>
<vertical-layout id="Level2" childmargin="10px">
<div>child2-1</div>
<div>child2-2</div>
<vertical-layout id="Level3" childmargin="5px">
<div>child3-1</div>
<div>child3-2</div>
<div>child3-3</div>
</vertical-layout>
<div>child2-3</div>
</vertical-layout>
<div>child1-4</div>
<div>child1-5</div>
</vertical-layout>

<style>
body {
font: 12px arial;
}
vertical-layout > *:not(:first-child) {
font-weight: bold;
margin-top: var(--childmargin);
}
vertical-layout::before {
content: "<vertical-layout " attr(id) " childmargin=" attr(childmargin);
}
vertical-layout > vertical-layout {
background: lightblue;
border-top: 4px dashed red;
}
vertical-layout > vertical-layout > vertical-layout {
background: lightcoral;
}
</style>

<script>
customElements.define("vertical-layout", class extends HTMLElement {
constructor() {
super()
.attachShadow({
mode: "open"
})
.innerHTML =
`<style>
:host {
display: flex;
flex-direction: column;
background:lightgreen;
padding-left:20px;
border:2px solid red;
}
::slotted(*){margin-left:20px}
:host([childmargin]) ::slotted(:not(:first-child)) {
color:blue;
}
</style>
<slot>
<slot></slot>
</slot>`;
}
connectedCallback() {
let margin = this.getAttribute("childmargin");
setTimeout(() => {
let children = [...this.querySelectorAll("*:not(:first-child)")];
children.map(child=>{
child.style.setProperty("--childmargin", margin);
child.append(` margin-top: ${margin}`);
})
});
}
});

</script>

Organizing multiple web components with seperation of concerns

  1. I've not seen this approach in any examples I've encountered so far, so wondering if is a good approach? Or there is a better approach than this in terms of organizing web components.

It's perfectly fine. Creating your elements programmatically has many advantages, mainly there is no need to query your own shadow root to get access to child elements/components. If need be, you can directly hold references or even create those in class properties, e.g.:

export class HelloWorld extends HTMLElement {
planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune']
.map(
planet => Object.assign(document.createElement('hello-planet'), { planet })
)
)

constructor() {
super().attachShadow({ mode: 'open' }).append(...this.planets);
}
}

Sidenote: Creating the shadow root can and should safely be done in the constructor.


  1. When I need template+styles, how can this approach be extended to read those from different files i.e. html and css in separate files (so we have separation of concerns)?

For CSS, we have CSS module scripts:

import styles from './hello-world.css' assert { type: 'css' }

then, in your constructor, do

constructor() {
// ...
this.shadowRoot.adoptedStylesheets.push(styles);
}

For HTML, this importing feature unfortunately is still work in progress.

Importing styles into a web component

Now direct <link> tag is supported in shadow dom.

One can directly use:

<link rel="stylesheet" href="yourcss1.css">
<link href="yourcss2.css" rel="stylesheet" type="text/css">

It has been approved by both whatwg and W3C.

Useful links for using css in shadow dom:

  • https://w3c.github.io/webcomponents/spec/shadow/#inertness-of-html-elements-in-a-shadow-tree
  • https://github.com/whatwg/html/commit/43c57866c2bbc20dc0deb15a721a28cbaad2140c
  • https://github.com/w3c/webcomponents/issues/628

Direct css link can be used in shadow dom.



Related Topics



Leave a reply



Submit