foreignObject only works when the user of the <svg-slot>Web Component
includes a valid SVG:
<svg-slot>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
<circle cx="50%" cy="50%" r="15%" fill="green"></circle>
</svg>
</svg-slot>
for the <template>:
<template>
<style>svg{width:40vw}</style>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
<circle cx="50%" cy="50%" r="25%" fill="red"></circle>
<foreignObject x="0" y="0" width="100%" height="100%">
<slot></slot>
</foreignObject>
</svg>
</template>
The <svg-slot> Web Component code required:
customElements.define('svg-slot', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode:'open'})
.append(document.querySelector('template').content.cloneNode(true));
}
});
But...
you want your Web Component users to write minimal semantic HTML:
<svg-slots>
<circle slot="foo" cx="50%" cy="50%" r="15%" fill="green"></circle>
<circle slot="bar" cx="50%" cy="50%" r= "5%" fill="gold"></circle>
</svg-slots>

That needs some extra work in the connectedCallback of the Web Component
Because <circle> in lightDOM now are Unknown HTML Elements.
So you need an extra step to turn everything in lightDOM into SVG (correct SVG NameSpace)
Which can then be injected into the (template) SVG in shadowDOM
<svg-slots>
<circle slot="foo" cx="50%" cy="50%" r="30%" fill="green"></circle>
<circle slot="bar" cx="50%" cy="50%" r="10%" fill="gold"></circle>
</svg-slots>
<template>
<style>svg{ height:180px }</style>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
<circle cx="50%" cy="50%" r="50%" fill="red"></circle>
<slot name="foo"></slot>
<slot name="bar"></slot>
</svg>
</template>
<script>
customElements.define('svg-slots', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode:'open'})
.append(document.querySelector('template').content.cloneNode(true));
setTimeout(()=>{ // make sure innerHTML is parsed
let svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
svg.innerHTML = this.innerHTML;
svg.querySelectorAll("*")
.forEach(el =>
this
.shadowRoot
.querySelector(`slot[name="${el.getAttribute("slot")}"]`)
?.replaceWith(el)
)
})
}
});
</script>
notes:
using the <slot> element; but not its functionality; it can be any (unknown) element.
the connectedCallback also fires when moving DOM nodes (eg. Drag Drop); you need extra code to prevent errors. attachShadow can be moved to the constructor
Can be re-factored to not use shadowDOM; then all SVGs can be styled with global CSS
but you will get a FOUC because <svg-slots> 'innerHTML' now is shown as regular DOM,
not lightDOM (optionally reflected/slotted into shadowDOM)
You can do a lot more with Unknown HTML Elements
<pie-chart>
<slice size="90" stroke="green">HTML</slice>
<slice size="1" stroke="red">JavaScript</slice>
<slice size="9" stroke="blue">CSS</slice>
</pie-chart>
creates a full SVG Pie Chart;
See: