Skip to content

JavaScript Tutorial Web APIs

MutationObserver

Lesson 213 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

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).

index.html
HTML
<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:

OptionWatches
childList: truechildren added or removed
attributes: trueattribute changes (limit with attributeFilter: ["class"])
characterData: truetext node content
subtree: truethe whole tree below, not just direct children
attributeOldValue: trueinclude the previous value

At least one of childList, attributes or characterData must be true.

The mutation records

PropertyMeaning
type"childList", "attributes" or "characterData"
targetthe node that changed
addedNodes, removedNodesfor childList
attributeName, oldValuefor attributes

Watching an attribute

index.html
HTML
<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 class or aria-* 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

script.js
JS
observer.disconnect();
dangerous
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.