I have developed a HTML web component, with a slot that uses a HTML theme
attribute to enable spacing out the child elements (by applying a margin
to them). I want to control the size of the spacing using a CSS custom property, --spacing-size
, set on the HTML style
attribute of each component instance.
class VerticalLayout extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
}
connectedCallback() {
this.shadowRoot.innerHTML =
`<style>
:host {
display: flex;
flex-direction: column;
}
:host([theme~="spacing"]) ::slotted(:not(:first-child)) {
margin-top: var(--spacing-size);
}
</style>
<slot></slot>`;
}
}
customElements.define("vertical-layout", VerticalLayout);
I run into problems when I add an instance of my component into the slot of another instance, because my CSS targets slotted elements to give them the margin. Since in this case the slotted element is my component, with a value of --spacing-size
meant for its children, it gets the margin meant for its children and not the margin needed inside the parent.
<vertical-layout theme="spacing" style="--spacing-size: 2em;">
<div>No margin on this, it is the first child</div>
<!-- I want a 2em margin on the next element (the size -->
<!-- from the layout it sits within), but it gets 0.2em -->
<!-- because --spacing-size has been redefined -->
<vertical-layout theme="spacing" style="--spacing-size: 0.2em;">
<div>No margin on this, it is the first child</div>
<div>0.2em margin above this</div>
</vertical-layout>
</vertical-layout>
I have created a codepen. See in the codepen I have overcome the issue by adding a second custom property, --parent-size
. The codepen illustrates the spacing I expect on the layouts, but is there a clever way to achieve the same behaviour with just one custom property?
See in the codepen, an additional complication is that I am explicitly setting the --spacing-size
to a default value in the CSS, to be applied when the theme is turned on but a size is not specified. I suppose this could make it pretty difficult to inherit whatever the value is in the parent...
I feel like the :host-context()
selector might be the answer, but I can't quite grasp how I could use it (and, since Safari doesn't support that, I would have to look for another solution anyway).
Took some time to fully understand what you want (and I could be wrong)
margin-top
for all CHILDREN (except the first child)<vertical-layout childmargin="2em">
<vertical-layout>
the element should have the margin-top
of its PARENT containerProblem 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
[...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 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));
});
}
});
<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>
Also look at ::part
https://developer.mozilla.org/en-US/docs/Web/CSS/::part