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