Skip to content

Mutation Observer

This lesson explains Mutation Observer with clear examples, use cases, and best practices so you can use HTML5 APIs confidently in real web applications.

HTML5 Mutation Observer API Overview

The HTML5 Mutation Observer API allows JavaScript to watch for changes in the DOM. It can detect when elements are added, removed, updated, or when attributes and text content change. It replaces older mutation events with a more efficient and asynchronous observer-based API.

Mutation Observer is commonly used in dynamic web applications, design systems, analytics, browser extensions, accessibility tooling, custom components, DOM monitoring, third-party widget integration, and applications that need to react when page content changes.

Feature Description
API Name Mutation Observer API
Main Object MutationObserver
Main Method observe()
Stop Method disconnect()
Records Method takeRecords()
Common Uses DOM changes, widgets, components, analytics, accessibility checks.

Basic Mutation Observer Example

// HTML5 Mutation Observer API

const target =
  document.querySelector("#app");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log("Mutation type:", mutation.type);
    });
  });

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

This example observes the #app element and detects when child elements are added or removed inside it.

How Mutation Observer Works

Mutation Observer watches a target element and sends a list of mutation records to a callback whenever configured DOM changes occur.

Step Description Example Syntax
Create Observer Create a new observer with a callback function. new MutationObserver(callback)
Select Target Select the DOM node to observe. document.querySelector("#app")
Configure Options Choose what type of DOM changes to watch. { childList: true, subtree: true }
Start Watching Begin observing the target node. observer.observe(target, options)
React to Changes Read mutation records inside the callback. mutation.type
Stop Watching Disconnect observer when no longer needed. observer.disconnect()

Mutation Observer Options

const options =
  {
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
    characterDataOldValue: true
  }

observer.observe(
  target,
  options
);
Option Description Common Use
childList Detects added or removed child nodes. Watch dynamic lists, widgets, page updates.
attributes Detects attribute changes. Watch class, style, aria, data attributes.
characterData Detects text node changes. Watch dynamic text updates.
subtree Watches all descendants of the target. Observe nested DOM changes.
attributeFilter Limits observation to specific attributes. Improve performance by watching selected attributes.
attributeOldValue Stores previous attribute value. Compare before and after values.
characterDataOldValue Stores previous text value. Compare text changes.

MutationRecord Properties

Property Description Available For
type Type of mutation. childList, attributes, characterData
target Node where the mutation occurred. All mutation types
addedNodes Nodes added to the DOM. childList
removedNodes Nodes removed from the DOM. childList
attributeName Name of changed attribute. attributes
oldValue Previous attribute or text value. attributes, characterData
previousSibling Previous sibling of added or removed nodes. childList
nextSibling Next sibling of added or removed nodes. childList

Watch Added and Removed Elements

const list =
  document.querySelector("#todoList");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log("Added:", node.textContent);
        }
      });

      mutation.removedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log("Removed:", node.textContent);
        }
      });
    });
  });

observer.observe(
  list,
  {
    childList: true
  }
);

This example detects when list items are added or removed from a to-do list.

Watch Attribute Changes

const button =
  document.querySelector("#saveButton");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log(
        "Changed attribute:",
        mutation.attributeName
      );

      console.log(
        "Old value:",
        mutation.oldValue
      );

      console.log(
        "New value:",
        mutation.target.getAttribute(
          mutation.attributeName
        )
      );
    });
  });

observer.observe(
  button,
  {
    attributes: true,
    attributeOldValue: true,
    attributeFilter: ["disabled", "aria-expanded", "class"]
  }
);

Attribute observation is useful for monitoring UI state changes such as disabled buttons, expanded menus, selected tabs, theme classes, and ARIA attributes.

Watch Text Changes

const status =
  document.querySelector("#status");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log(
        "Old text:",
        mutation.oldValue
      );

      console.log(
        "New text:",
        mutation.target.textContent
      );
    });
  });

observer.observe(
  status.firstChild,
  {
    characterData: true,
    characterDataOldValue: true
  }
);

This example watches a text node for changes. It is useful for live status messages, counters, notification labels, and dynamic text updates.

Observe an Entire Subtree

const app =
  document.querySelector("#app");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      console.log(
        "Change detected:",
        mutation.type
      );
    });
  });

observer.observe(
  app,
  {
    childList: true,
    attributes: true,
    subtree: true
  }
);

The subtree option watches the target and all nested descendants. Use it carefully because observing large DOM trees can affect performance.

Detect Third-Party Widget Changes

const widgetContainer =
  document.querySelector("#widget-container");

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (
          node.nodeType === Node.ELEMENT_NODE &&
          node.matches(".third-party-widget")
        ) {
          console.log("Widget loaded.");
          node.classList.add("widget-ready");
        }
      });
    });
  });

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

Mutation Observer can detect when third-party scripts inject content into a page and then apply custom logic or styles.

Accessibility Audit Example

const observer =
  new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (
          node.nodeType === Node.ELEMENT_NODE &&
          node.matches("img:not([alt])")
        ) {
          console.warn(
            "Image missing alt text:",
            node
          );
        }
      });
    });
  });

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

Development tools and accessibility helpers can use Mutation Observer to detect accessibility issues in dynamically inserted content.

Mutation Observer Methods

Method Description Example Syntax
observe() Starts observing a DOM node. observer.observe(target, options)
disconnect() Stops observing all targets. observer.disconnect()
takeRecords() Returns pending mutation records. observer.takeRecords()

Stop Observing Changes

// Stop observing when done

observer.disconnect();

Use disconnect() when the observer is no longer needed, such as when a component unmounts or a modal closes.

Common Mutation Observer Use Cases

Use Case How It Helps
Dynamic UI Components React when elements are added or removed.
Third-Party Widgets Detect injected scripts, ads, chats, or embeds.
Accessibility Tools Check dynamically inserted content for issues.
Analytics Track dynamically loaded content impressions.
Browser Extensions Monitor and modify page content safely.
Custom Elements React to child or attribute changes.
Testing Utilities Wait for async DOM updates in tests.
Design Systems Detect misuse or dynamically added components.

Mutation Observer Best Practices

  • Observe the smallest possible DOM subtree.
  • Use attributeFilter when watching attributes.
  • Disconnect observers when no longer needed.
  • Avoid heavy work inside the observer callback.
  • Batch or debounce expensive updates.
  • Use subtree carefully on large pages.
  • Validate node types before using DOM methods.
  • Prefer framework lifecycle hooks when working inside React, Vue, or Angular.

Common Mutation Observer Mistakes

  • Observing the entire document when a smaller container is enough.
  • Forgetting to call disconnect().
  • Running expensive DOM queries for every mutation.
  • Watching attributes without using attributeFilter.
  • Creating infinite loops by changing the same DOM being observed.
  • Assuming every added node is an element node.
  • Using Mutation Observer when a simple event listener is enough.
  • Using it instead of framework lifecycle methods unnecessarily.

Mutation Observer vs Intersection Observer vs Resize Observer

API Detects Best Use
Mutation Observer DOM node, attribute, and text changes. Dynamic DOM updates.
Intersection Observer Element visibility in viewport or container. Lazy loading, infinite scroll, analytics.
Resize Observer Element size changes. Responsive components and layout monitoring.

Key Takeaways

  • The Mutation Observer API watches DOM changes.
  • Use new MutationObserver() to create an observer.
  • Use observe() with options to start watching a target.
  • Use disconnect() to stop observing and avoid memory leaks.
  • Mutation records describe what changed in the DOM.
  • Use it carefully because observing large DOM trees can impact performance.

Pro Tip

Mutation Observer is powerful for detecting DOM changes, but it should not replace normal application state management. In React, Vue, Angular, or Svelte apps, prefer component lifecycle and state updates first. Use Mutation Observer mainly for third-party DOM changes, browser extensions, dynamic embeds, and low-level DOM monitoring.