Good to see more people are combining Custom Elements and SVG, they are a good match
Your core problem was the SVG NameSpace, so Paul his answer is correct.
Some additional comments:
Your constructor can be optimized:
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const container = document.createElementNS('http://www.w3.org/2000/svg','svg');
container.setAttribute('width', '400');
container.setAttribute('height', '300');
shadow.appendChild (container);
}
super()
returns the element scope
Google documentation that says to use super()
first is wrong,
they mean: Call super()
before you can use the 'this' scope reference.
attachShadow()
boths sets AND returns this.shadowRoot
for free
.appendChild()
returns the created element
So you can chain everything:
constructor() {
const container = super()
.attachShadow({mode:'open'})
.appendChild (document.createElementNS('http://www.w3.org/2000/svg','svg'));
container.setAttribute('width', '400');
container.setAttribute('height', '300');
}
If you do not do anything special with this in memory Custom Element,
you can scrap the whole constructor
and create the element with HTML in the connectedCallback
connectedCallback(){
this.innerHTML = `<svg width='400' height='300'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'></svg>`;
}
Note: I also ditched shadowDOM above; if you do want shadowDOM its:
connectedCallback(){
this.attachShadow({mode:"open"})
.innerHTML = `<svg width='400' height='300'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'></svg>`;
}
There is another sizing problem
Depending on your Font family and size there will be a gutter below your inline-block SVG Custom Element (see RED below) to allow for pgjq characters that stick out below the base line.

To counter that you have to set vertical-align: top
on the SVG element:
<style>
body { font-size: 3em }
svg-circle { background: RED }
svg-circle svg {
background: grey;
display: inline-block;
width: 80px;
}
#correct svg { vertical-align: top }
</style>
<div>
<svg-circle radius="40%" color="green"></svg-circle>
<svg-circle x="50%" y="100%" color="blue"></svg-circle>
<svg-circle></svg-circle>
</div>
<div id="correct">
<svg-circle radius="40%" color="green"></svg-circle>
<svg-circle x="50%" y="100%" color="blue"></svg-circle>
<svg-circle></svg-circle>
</div>
<script>
customElements.define("svg-circle", class extends HTMLElement {
static get observedAttributes() { return ["x", "y", "radius", "color"] }
connectedCallback() { this.render() }
attributeChangedCallback() { this.render() }
render() {
let [x = "50%",y = "50%",radius = "50%", color = "rebeccapurple"] =
this.constructor.observedAttributes.map(x => this.getAttribute(x) || undefined);
this.innerHTML = `<svg viewBox='0 0 96 96'>
<circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/></svg>`;
}
});
</script>
Note: There is no shadowDOM attached to <svg-circle>
, so the SVG inside can be styled with global CSS
Make it an IMG
If you do not want any CSS bleeding, and want to work with the SVG as if it is an image,
without pointer-events issues, then create an IMG:
this.innerHTML = `<img src="data:image/svg+xml,<svg viewBox='0 0 96 96'>
<circle cx='${x}' cy='${y}' r='${radius}' fill='${color}'/>
</svg>">`.replace(/#/g, "%23");
Note: The # is the only character that needs to be escaped here. In CSS url()
usage you also need to escape the < and >
Add a grid
If you are going to create an Icon Toolbar or Chessboard like layout with SVGs, add a grid:
#correct {
display: grid;
grid-template-columns: repeat(3, 80px);
}
svg-circle svg {
background: grey;
display: inline-block;
width: 100%;
height: 100%;
}