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 themargin-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
setTimeout
is required because all child have not been parsed yet when theconnectedCallback
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));
});
}
});
<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
- 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.
- 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
Making Buttons - <Button> or <Div>
Counter Keeps Resetting in HTML/CSS
Necessary to Add Link Tag for Favicon.Ico
Nested Sticky Element with Zero Left Does Not Sticky
Aligning Elements Left, Center and Right in Flexbox
Ie Offsetting and Ignoring Height/Width of Anchor Focus Outlines
Mobile Safari 5Mb HTML5 Application Cache Limit
Vertical Align Text After Font Icon
Why Does Angularjs Ng-View Not Work Locally
CSS Flexbox | Reordering Elements in Mobile
Why Doesn't Margin:Auto Center an Image
Embedding Image in Email with Vba
How to Restrict My Input Type="File" to Accept Only Png Image Files Not Working in Firefox
Open PDF File in New Window from Variable Path Name in Gsp Page
Wrap Table Row to The Next Line
Fill a Div with an Image Respecting Aspect Ratio
Break Long Word in Table Cell with Percentage Widths
How to Make a Div Take The Full Width of The Page When It Is Inside Another Div That Have 90% Width