Skip to content

Instantly share code, notes, and snippets.

@VivianVerdant
Forked from mindplay-dk/waitForElement.js
Last active April 29, 2025 22:31
Show Gist options
  • Save VivianVerdant/3f4bdb60884f7b9fe8868a834eb5668f to your computer and use it in GitHub Desktop.
Save VivianVerdant/3f4bdb60884f7b9fe8868a834eb5668f to your computer and use it in GitHub Desktop.
wait_for_element function (wait for element matching selector)
let animationCounter = 0;
function wait_for_element(selector, func, once, parent){
parent = parent || document;
once = (once == undefined ? true : false);
if (once) {
_wait_for_element_once(parent, selector).then(func);
} else {
_wait_for_element(parent, selector, func);
}
}
function _wait_for_element_once(parent, selector) {
return new Promise((resolve) => {
const elem = parent.querySelector(selector);
if (elem) {
resolve(elem); // already in the DOM
return
}
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(event.target);
}
};
function cleanUp() {
document.removeEventListener("animationstart", eventListener);
document.head.removeChild(style);
}
document.addEventListener("animationstart", eventListener, false);
});
}
function _wait_for_element(parent, selector, func) {
const elem = parent.querySelectorAll(selector);
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) {
func(event.target);
}
};
document.addEventListener("animationstart", eventListener, false);
}

Modified the original for use in my userscripts

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:

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