I would like to append an SVG Path element dynamically via JavaScript and immediately after the element has been appended to the HTML DOM execute a transition via CSS.
My set-up (as shown in the below example): The SVG element to which the Path element is added sits inside a DIV element in the HTML.
The idea is that after appending the Path element to the SVG element, I add a class to the DIV to initialise the transition. To make sure that the order of appending the path element and adding the class is correct, I use a promise. Since I tried to make sure that the element is first appended to the DOM and class is only then added to the DIV element, I would expect the transition to work.
However, I observe that the transition does not work, not even when I add a delay to it (via CSS), and the element will immediately receive the styling from the end of the transition.
If I add a timeout after the path element has been appended and before the Div gets the class, it works. But this somehow seems like a hack to me. I also observed that if I append multiple Path elements to the SVG element, only the transition for the last child is broken.
My question therefore is: Why is the transition not working as expected? Is there a more elegant way to achieve what I want than adding the timeout?
This is a code example to illustrate my problem:
const animatePath = () => {
return new Promise((resolve, reject) => {
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathElement.setAttribute('d', 'M0,0H500');
pathElement.setAttribute('stroke', 'black');
pathElement.setAttribute('stroke-width', 10);
pathElement.setAttribute('fill', 'none');
document.getElementById('svgElement').appendChild(pathElement);
resolve();
});
};
document.getElementById('append-toggle').addEventListener('click', (event) => {
animatePath().then(() => {
document.getElementById('wrapper').classList.add('show');
}).catch((error) => {
console.log(error);
return Promise.reject(error);
});
}, false);
let waitTimeout = false;
document.getElementById('append-wait-toggle').addEventListener('click', (event) => {
animatePath().then(() => {
clearTimeout(waitTimeout);
waitTimeout = setTimeout(() => {
document.getElementById('wrapper').classList.add('show');
}, 250);
}).catch((error) => {
console.log(error);
return Promise.reject(error);
});
}, false);
document.getElementById('toggle').addEventListener('click', (event) => {
document.getElementById('wrapper').classList.toggle('show');
});
document.getElementById('reset').addEventListener('click', (event) => {
document.getElementById('wrapper').classList.remove('show');
while(document.getElementById('svgElement').lastChild){
document.getElementById('svgElement').lastChild.remove();
}
});
#wrapper path {
opacity: 0;
transition: opacity 2s ease;
}
#wrapper.show path {
opacity: 1;
}
#wrapper.show {
outline: 1px solid red;
}
<div id="wrapper">
<svg width="500px" height="20px" viewBox="0 0 500 20" id="svgElement"></svg>
</div>
<br />
<button id="append-toggle">Append path and then toggle class</button>
<br />
<button id="append-wait-toggle">Append path, wait and then toggle class</button>
<br />
<button id="toggle">Toggle class</button>
<br />
<button id="reset">Reset</button>
Note: I need to animate the Path element and not one of its parents, because what I want to animate in my project is not the opacity of the element but the stroke-dashoffset
which I get into the CSS via style="--path-length: ${pathElement.getTotalLength()}"
. I first thought that this might be the cause of the problem, but as the above example shows, it is not.
Edit
Part of my question, namely as to why this happens, has been answered here. A solution is obviously to force the browser to reflow after the element has been appended to the DOM. However, the linked answers don't relate to SVG.
So, my question would still be: Is there an optimal way to force a reflow especially for SVG elements?