MutationObserver lets you react when the DOM changes: elements added or removed, attributes changed, text edited. It is useful when you do not control the code that changes the page (a third-party widget, an editor, a framework).
<ul id="list"></ul>
<button id="add">Add item</button>
<p id="log"></p>
<script>
const list = document.querySelector("#list");
const log = document.querySelector("#log");
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === "childList") {
log.textContent = `${mutation.addedNodes.length} node(s) added, ${list.children.length} items now`;
console.log("childList:", mutation.addedNodes.length, "added");
}
}
});
observer.observe(list, { childList: true });
let n = 0;
document.querySelector("#add").addEventListener("click", () => {
const li = document.createElement("li");
li.textContent = "Item " + (++n);
list.append(li); // the observer is notified
});
</script>
Configuration
observer.observe(target, options). Choose what to watch:
| Option | Watches |
|---|---|
childList: true | children added or removed |
attributes: true | attribute changes (limit with attributeFilter: ["class"]) |
characterData: true | text node content |
subtree: true | the whole tree below, not just direct children |
attributeOldValue: true | include the previous value |
At least one of childList, attributes or characterData must be true.
The mutation records
| Property | Meaning |
|---|---|
type | "childList", "attributes" or "characterData" |
target | the node that changed |
addedNodes, removedNodes | for childList |
attributeName, oldValue | for attributes |
Watching an attribute
<div id="panel" class="closed">Panel</div>
<script>
const panel = document.querySelector("#panel");
new MutationObserver(records => {
for (const r of records) {
console.log(r.attributeName, "changed from", r.oldValue, "to", panel.className);
}
}).observe(panel, { attributes: true, attributeFilter: ["class"], attributeOldValue: true });
panel.classList.replace("closed", "open");
</script>
Batching and timing
Observers are asynchronous and batched: callbacks run after the current code finishes, as a microtask, with all the records together. That makes them efficient. Use observer.takeRecords() to read pending ones early.
Avoid loops
If your callback changes the DOM you are observing, it triggers itself. Guard against it, or disconnect while you make the change.
When to use it
- A script that must react to content added by something else.
- Waiting for an element to appear (a widget that loads late).
- Tracking
classoraria-*changes made elsewhere. - Testing tools and browser extensions.
When you control the code that changes the page, there is no need for it. Use a function call or a custom event instead.
Stop watching
observer.disconnect();
Do not observe too widely
subtree: true on document.body with all options on can create a lot of callbacks and hurt performance. Observe the smallest part of the page that you need.