A one-shot entry animation replays every time its element is recreated, so a wholesale re-render turns it into a flicker
Fact
A CSS animation belongs to the element, not to the data it is showing. Replacing a node restarts the animation from the beginning, so a render function that rebuilds a subtree from state replays every entry animation inside it on every state change — including changes that have nothing to do with the animated elements.
The result is not a visible animation. Each replay is cut short by the next render, so the elements sit permanently in the early frames of the keyframes: faded, offset, or half-scaled.
Why it matters
It reads as a CSS bug — wrong opacity, wrong transform, a broken theme — and the animation is correct. It is also invisible while nothing else is happening, so it only appears once the surface starts updating often, which is exactly when a screenshot is hardest to interpret.
How to apply
Build each element once and update it in place. Rebuild only the nodes whose content actually changed, gated on a signature of that content:
const sig = items.map((i) => i.id).join(','); if (sig !== node.dataset.sig) { node.dataset.sig = sig; node.replaceChildren(...items.map(render)); }- When an animation should replay on demand, restart it deliberately: remove the class, force a reflow with
void el.offsetWidth, add it back. - A framework that keys its children solves this by identity; the trap is the hand-written
replaceChildren()render, which has no identity at all.