MutationObserver
Some things you cannot polyfill: knowing when the DOM has changed. MutationObserver is the official, batched, low-overhead API for reacting to DOM mutations — new nodes, removed nodes, attribute changes, text changes — whether you made them or somebody else's code did. It replaces the old (and deprecated) Mutation Events with a model that does not fire callbacks during the mutation itself.
The shape of the API
const observer = new MutationObserver((mutations, obs) => {
for (const m of mutations) {
console.log(m.type, m.target, m.addedNodes, m.removedNodes);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});You pick a target and a configuration. The callback fires after the browser is done batching mutations — typically right before paint — with an array of MutationRecords.
The configuration options
childList— watch for added or removed children of the target.subtree— extend any watched event to the entire descendant tree, not just direct children.attributes— watch for attribute changes on the target (and descendants, ifsubtree).attributeOldValue— also record the previous attribute value in the record.attributeFilter— array of attribute names to watch; ignores the rest.characterData— watch for changes to the text inside text nodes.characterDataOldValue— record the previous text.
The MutationRecord
// childList example m.type; // "childList" m.target; // the parent whose children changed m.addedNodes; // NodeList m.removedNodes; // NodeList m.previousSibling; // sibling before the changed nodes m.nextSibling; // attributes example m.type; // "attributes" m.attributeName; // "class" m.attributeNamespace; m.oldValue; // present if attributeOldValue: true // characterData example m.type; // "characterData" m.target; // the Text node m.oldValue; // previous text
Watching for an element to appear
A common use is "do X as soon as element Y exists" — for instance reacting to third-party widgets that inject into the page asynchronously.
waitFor.js
function waitFor(selector, root = document.body) {
return new Promise((resolve) => {
const existing = root.querySelector(selector);
if (existing) return resolve(existing);
const obs = new MutationObserver(() => {
const found = root.querySelector(selector);
if (found) {
obs.disconnect();
resolve(found);
}
});
obs.observe(root, { childList: true, subtree: true });
});
}
const widget = await waitFor("#third-party-widget");Reacting to attribute changes
theme.js
const obs = new MutationObserver((records) => {
for (const r of records) {
if (r.attributeName === "data-theme") {
console.log("theme is now:", document.documentElement.dataset.theme);
}
}
});
obs.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});The attributeFilter keeps you from being woken up by every unrelated attribute change on the root — much cheaper than filtering inside the callback.
Detecting text edits in a contenteditable
const editor = document.querySelector("[contenteditable]");
const obs = new MutationObserver((records) => {
for (const r of records) {
if (r.type === "characterData") {
console.log("text changed from", r.oldValue, "to", r.target.data);
}
}
});
obs.observe(editor, {
characterData: true,
characterDataOldValue: true,
subtree: true,
});takeRecords and disconnect
Two extra methods are worth knowing.
observer.takeRecords()— synchronously returns and clears any queued mutation records that the callback has not yet seen. Useful when you want to "flush" before disconnecting.observer.disconnect()— stop observing. Any pending records are dropped (unless you take them first).
const pending = obs.takeRecords(); obs.disconnect(); handle(pending); // be sure you got everything
Real use cases
Re-running a syntax highlighter when a markdown component re-renders.
Auto-saving a contenteditable after edits, debounced.
Detecting that a third-party script injected a banner / cookie modal and styling it.
Polyfilling custom element lifecycle in older browsers.
Test utilities that wait for "the DOM looks like this" before asserting.