W3docs

JavaScript MutationObserver API

Learn the JavaScript MutationObserver API: how to watch DOM changes, the observe/disconnect/takeRecords methods, and reading MutationRecords.

The MutationObserver API in JavaScript lets you watch a part of the DOM and run a callback whenever something inside it changes — a node is added or removed, an attribute is edited, or text content is updated. Unlike the obsolete mutation events it replaced, MutationObserver batches changes and delivers them asynchronously, so it doesn't block the page or fire a separate event for every tiny edit.

This guide covers what you can observe, the three methods every observer exposes, the shape of a MutationRecord, real-world use cases, common gotchas, and an interactive example you can run.

When would you use a MutationObserver?

You reach for a MutationObserver when content you don't control changes and you need to react to it:

  • Third-party / injected content — a widget, ad, or CMS-rendered block appears in the DOM and you need to style or enhance it.
  • Detecting when an element finally exists — waiting for a node a framework renders later, instead of polling with setInterval.
  • Syncing UI to attribute changes — reacting when class, style, disabled, or a data-* attribute changes on an element you can't add a listener to.
  • Auto-resizing or re-measuring — recalculating layout when children are inserted into a container.
  • Keeping a model in sync with contenteditable text.

If you only need to know when an element enters or leaves the viewport, use an IntersectionObserver instead — it is purpose-built and cheaper for that job.

The three methods

Every observer instance exposes exactly three methods:

MethodWhat it does
observe(target, options)Start watching target using an options object. Call it again with a different target to watch more than one node with the same observer.
disconnect()Stop watching all targets. The callback will no longer fire. Always call this when you're done to avoid memory leaks.
takeRecords()Synchronously return (and clear) any pending MutationRecords that haven't been delivered to the callback yet. Useful right before disconnect() so you don't drop the last batch.

The options object passed to observe() must enable at least one of childList, attributes, or characterData, or the call throws a TypeError.

OptionMeaning
childListWatch for added/removed direct child nodes.
attributesWatch for attribute changes.
characterDataWatch for changes to text-node data.
subtreeExtend the above to all descendants, not just the target.
attributeOldValueRecord the previous attribute value (implies attributes).
characterDataOldValueRecord the previous text value (implies characterData).
attributeFilterArray of attribute names to watch — ignore the rest.

Mutation Observer Example

Here's a basic example to help you see how a Mutation Observer works by visually indicating changes in the DOM.

<!DOCTYPE html>
<html>
<head>
  <title>Exploring DOM Changes: Live Examples with Mutation Observers</title>
</head>
<body>
  <div id="target" style="background-color: lightgray; padding: 10px;">
    Watch this space for changes!
  </div>
  <button style="margin-top: 10px;" onclick="addNewElement(); changeAttribute();">Add New Element and Change Color</button>
  <div id="log" style="margin-top: 20px;"></div>

  <script>
    // Get the element to observe
    const targetNode = document.getElementById('target');

    // Define configurations for the observer
    const config = { attributes: true, childList: true, subtree: true, attributeOldValue: true };

    // Callback function to execute when mutations are observed
    const callback = function(mutationsList, observer) {
      for (const mutation of mutationsList) {
        const message = document.createElement('p');
        if (mutation.type === 'childList') {
          message.textContent = 'A child node has been added or removed.';
          message.style.color = 'green';
        } else if (mutation.type === 'attributes') {
          message.textContent = 'The ' + mutation.attributeName + ' attribute was modified.';
          message.style.color = 'blue';
        }
        document.getElementById('log').appendChild(message);
      }
    };

    // Create an observer instance linked to the callback function
    const observer = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    observer.observe(targetNode, config);

    // Function to add new elements
    function addNewElement() {
      const newElement = document.createElement('div');
      newElement.textContent = 'New element added!';
      targetNode.appendChild(newElement);
    }

    // Function to change attributes
    function changeAttribute() {
      const currentColor = targetNode.style.backgroundColor;
      targetNode.style.backgroundColor = currentColor === 'lightgray' ? 'lightblue' : 'lightgray';
    }
  </script>
</body>
</html>

This example showcases how to use a Mutation Observer to detect and respond to changes in a webpage's Document Object Model (DOM). Here's what each part of the JavaScript does and what you can expect to see when you interact with the example:

  1. Setup Mutation Observer:
    • Target Node: This is the DOM element you want to watch. In this case, it's the div with ID target.
    • Configurations: These specify what types of changes you want to monitor:
      • attributes: The observer will look for changes to attributes (like style or class).
      • childList: It will check for the addition or removal of child elements (like new divs being added).
      • subtree: This ensures the observer checks not just the target element, but also its descendants. Note that subtree only takes effect if childList or attributes is also enabled.
      • attributeOldValue: This records the previous value of any attribute that gets modified (useful for tracking changes).
  2. Define a Callback Function:
    • This function executes each time the observer detects a change based on the configurations set.
    • It loops through all detected mutations and creates a log message for each one:
      • If a child element is added or removed, it logs "A child node has been added or removed." in green text.
      • If an attribute is changed (like the background color), it logs "The mutation.attributeName attribute was modified." in blue text.
  3. Observer Instance:
    • The Mutation Observer is created and linked to the callback function.
  4. Start Observing:
    • The observer begins monitoring the target div for any changes specified in the configurations.
  5. Interactive Functions:
    • Add New Element: Triggered by a button click, this function adds a new div with text "New element added!" inside the target div.
    • Change Attribute: Also triggered by the same button click, this function toggles the background color of the target div between 'lightgray' and 'lightblue'. Note: While inline onclick works for this example, using addEventListener is recommended for cleaner code separation in production.

Expected Results:

  • Adding a New Element:
    • Each time you click the button, a new div is added. This triggers the observer's childList check, and you will see a green message saying "A child node has been added or removed."
  • Changing an Attribute:
    • The same button click will change the background color of the target div. This triggers the observer's attribute check. You will see a blue message indicating which attribute was changed ("The style attribute was modified.").

This example effectively demonstrates how Mutation Observers can be used to monitor and log changes in the DOM, providing real-time feedback on what's happening within the webpage.

Reading a MutationRecord

The callback receives an array of MutationRecord objects — one per detected change. The most useful properties are:

  • type"childList", "attributes", or "characterData".
  • target — the node the mutation affected.
  • addedNodes / removedNodesNodeLists of inserted/removed nodes (for childList).
  • attributeName — the changed attribute (for attributes).
  • oldValue — the previous value, but only if attributeOldValue or characterDataOldValue was enabled.
const observer = new MutationObserver((records) => {
  for (const record of records) {
    if (record.type === "attributes") {
      console.log(`${record.attributeName} changed from "${record.oldValue}"`);
    } else if (record.type === "childList") {
      console.log(`+${record.addedNodes.length} / -${record.removedNodes.length} nodes`);
    }
  }
});

A practical pattern: wait for an element to appear

A common real-world use is resolving a Promise the moment a node shows up in the DOM — far better than polling. The observer disconnects itself as soon as it finds the element:

function waitForElement(selector) {
  return new Promise((resolve) => {
    const existing = document.querySelector(selector);
    if (existing) return resolve(existing);

    const observer = new MutationObserver(() => {
      const el = document.querySelector(selector);
      if (el) {
        observer.disconnect(); // stop watching once found
        resolve(el);
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });
  });
}

// Usage with async/await:
// const card = await waitForElement(".lazy-card");

This pairs naturally with async/await and Promises.

Gotchas and best practices

  • The callback is asynchronous and batched. Mutations are queued as microtasks and delivered after the current script finishes — see microtasks and the event loop. You won't get a record for every individual change; you get them grouped.
  • Your callback runs after the change. You're notified of what already happened — you can't cancel or prevent a mutation the way you might preventDefault() on an event.
  • oldValue is opt-in. If you read record.oldValue without enabling attributeOldValue/characterDataOldValue, it's null.
  • Avoid mutating the observed subtree inside the callback unless you intend to — doing so can trigger more records and create a feedback loop.
  • Always disconnect(). A live observer keeps its target reachable, so forgetting to disconnect leaks memory. Disconnect when the component unmounts or the work is done.
  • takeRecords() before disconnecting if the last batch matters — disconnect() discards undelivered records.

Conclusion

Mutation Observers are a critical part of the JavaScript toolkit, offering dynamic solutions for managing DOM changes efficiently. They empower developers to build responsive, interactive web applications that react seamlessly to user interactions and programmatic DOM modifications. While they are powerful, it's essential to use Mutation Observers judiciously to maintain optimal performance and user experience. By carefully selecting which mutations to observe, minimizing the overhead in mutation callbacks, and calling observer.disconnect() when the observer is no longer needed to prevent memory leaks, developers can leverage Mutation Observers to enhance site functionality without compromising on efficiency. Understanding and applying these principles allows for the creation of advanced, user-friendly web interfaces that stand out in the modern digital landscape.

Practice

Practice
Which of the following statements are true regarding the JavaScript Mutation Observer?
Which of the following statements are true regarding the JavaScript Mutation Observer?
Was this page helpful?