mv3-wait-for-element
v1.0.0
Published
Wait for a DOM element to appear or disappear — tiny MutationObserver-backed helper for Chrome extension content scripts on Manifest V3 (MV3). Zero dependencies, fully typed.
Maintainers
Readme
mv3-wait-for-element
Wait for a DOM element to appear or disappear — a tiny MutationObserver-backed helper purpose-built for Chrome extension content scripts targeting SPAs (Gmail, Twitter/X, Notion, Linear, …) on Manifest V3 (MV3). Zero dependencies, fully typed.
import { waitForElement, waitForElementGone } from "mv3-wait-for-element";
const compose = await waitForElement<HTMLButtonElement>('[role="button"][gh="cm"]', {
timeoutMs: 10_000,
});
compose.click();
// or with a predicate
const ready = await waitForElement(() => {
const list = document.querySelector(".list");
return list?.dataset.ready === "true" ? list : null;
});
await waitForElementGone(".loading-spinner", { timeoutMs: 5000 });Why
Content scripts run early — usually before the host SPA has finished
rendering. You can't just call document.querySelector and assume the
element exists. Every extension that injects into Gmail, Twitter, Notion,
or any modern web app reinvents this MutationObserver boilerplate, often
with bugs (no timeout, no cleanup, no abort). This package wraps it
correctly in ~50 LOC.
Install
pnpm add mv3-wait-for-element
# or: npm i mv3-wait-for-element
# or: yarn add mv3-wait-for-elementUsage
Wait by CSS selector
import { waitForElement } from "mv3-wait-for-element";
const sendBtn = await waitForElement<HTMLButtonElement>(
'div[role="button"][aria-label*="Send"]',
);
sendBtn.click();Wait by predicate
For "exists and in the right state":
const list = await waitForElement(() => {
const el = document.querySelector<HTMLUListElement>(".thread-list");
return el && el.children.length > 0 ? el : null;
});Wait until something disappears
Useful for "wait for the loading spinner to go away":
import { waitForElementGone } from "mv3-wait-for-element";
await waitForElementGone(".spinner");
// now the page has finished loadingAbort with AbortSignal
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
const el = await waitForElement(".widget", { signal: controller.signal });
} catch (err) {
if (err instanceof WaitForElementError && err.reason === "aborted") {
// user navigated away — clean up
}
}API
waitForElement<T>(probe, options?) → Promise<T>
| Argument | Type | Notes |
| --- | --- | --- |
| probe | string \| ((root) => T \| null) | CSS selector, or a function returning the element when "ready". |
| options.root | Document \| Element | Where to observe. Defaults to document. |
| options.timeoutMs | number | Rejects after this many ms. Default 10000. |
| options.signal | AbortSignal | Abort the wait early. |
waitForElementGone(probe, options?) → Promise<void>
Resolves once the element no longer matches.
WaitForElementError
Thrown on timeout or abort. The .reason property is "timeout" or
"aborted".
Demo extension
A runnable demo lives in example/. It loads as an unpacked
Chrome extension that injects a banner into every https:// page after
waiting for the page's <h1> to appear — proving the wait works on
dynamically-rendered SPAs.
cd example
pnpm install
pnpm build
# then load example/dist/ via chrome://extensions → "Load unpacked"See example/README.md for full instructions.
Related packages
Part of a small MV3 toolkit for Chrome / Edge / Firefox extensions by @graybearo:
mv3-content-bridge— content-script ↔ page-context typed bridgemv3-message-router— typed messaging between popup, content, and service workermv3-storage— typedchrome.storagewrappermv3-keepalive— service-worker keepalive + durable alarmschrome-extension-vite-react— Vite + React + TS MV3 starterawesome-mv3— curated list of MV3 tools, libraries, and resources
License
MIT — see LICENSE.
