Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active April 29, 2025 22:34
Show Gist options
  • Save mindplay-dk/d2c25eb96fb707a4749e08666e8aea31 to your computer and use it in GitHub Desktop.
Save mindplay-dk/d2c25eb96fb707a4749e08666e8aea31 to your computer and use it in GitHub Desktop.
waitForElement function (wait for element matching selector)
let animationCounter = 0;
export function waitForElement(selector) {
return new Promise((resolve) => {
const elem = document.querySelector(selector);
if (elem) {
resolve(elem); // already in the DOM
}
const animationName = `waitForElement__${animationCounter++}`;
const style = document.createElement("style");
const keyFrames = `
@keyframes ${animationName} {
from { opacity: 1; }
to { opacity: 1; }
}
${selector} {
animation-duration: 1ms;
animation-name: ${animationName};
}
`;
style.appendChild(new Text(keyFrames));
document.head.appendChild(style);
const eventListener = (event) => {
if (event.animationName === animationName) {
cleanUp();
resolve(document.querySelector(selector));
}
};
function cleanUp() {
document.removeEventListener("animationstart", eventListener);
document.head.removeChild(style);
}
document.addEventListener("animationstart", eventListener, false);
});
}

What is this witchcraft? ๐Ÿ˜„

I needed a way to wait for an HTML element, matching a given CSS selector, to get added to the DOM.

waitForElement("#popup.annoying").then(el => {
  el.remove();
});

I tried the usual approaches with MutationObserver, polling, etc. - none of it responded quickly enough, many of the existing solutions have performance issues, and a few of them don't actually work for selectors like ul.foo > li.bar if the foo class gets added after the li.bar elements have already been added.

Then it hit me - the CSS engine is already doing this work using extremely sophisticated optimizations, why don't we just let the CSS engine do the work? Inject a "CSS animation", and wait for the "animation" to start. It's a rugged approach, no doubt - but it's simple, and fast.

On the "fast" part, I can't actually prove this - I have no idea how you'd even benchmark something like this. But the more common approaches, like searching the entire DOM in response to mutations or timers, definitely made my script noticeably slower.

Prior art:

@VivianVerdant
Copy link

After trying several similar functions, this one has worked the best by far.
I did make some modifications to it for my own use, mainly to add a "once" option, so it can be set to target only the first found instance, or all instances matching the selector.
My version: https://gist.github.com/VivianVerdant/3f4bdb60884f7b9fe8868a834eb5668f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment