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 adata-*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:
| Method | What 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.
| Option | Meaning |
|---|---|
childList | Watch for added/removed direct child nodes. |
attributes | Watch for attribute changes. |
characterData | Watch for changes to text-node data. |
subtree | Extend the above to all descendants, not just the target. |
attributeOldValue | Record the previous attribute value (implies attributes). |
characterDataOldValue | Record the previous text value (implies characterData). |
attributeFilter | Array 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:
- Setup Mutation Observer:
- Target Node: This is the DOM element you want to watch. In this case, it's the
divwith IDtarget. - 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 thatsubtreeonly takes effect ifchildListorattributesis also enabled.attributeOldValue: This records the previous value of any attribute that gets modified (useful for tracking changes).
- Target Node: This is the DOM element you want to watch. In this case, it's the
- 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.attributeNameattribute was modified." in blue text.
- Observer Instance:
- The Mutation Observer is created and linked to the callback function.
- Start Observing:
- The observer begins monitoring the
targetdiv for any changes specified in the configurations.
- The observer begins monitoring the
- Interactive Functions:
- Add New Element: Triggered by a button click, this function adds a new div with text "New element added!" inside the
targetdiv. - Change Attribute: Also triggered by the same button click, this function toggles the background color of the
targetdiv between 'lightgray' and 'lightblue'. Note: While inlineonclickworks for this example, usingaddEventListeneris recommended for cleaner code separation in production.
- Add New Element: Triggered by a button click, this function adds a new div with text "New element added!" inside the
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
targetdiv. This triggers the observer's attribute check. You will see a blue message indicating which attribute was changed ("The style attribute was modified.").
- The same button click will change the background color of the
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/removedNodes—NodeLists of inserted/removed nodes (forchildList).attributeName— the changed attribute (forattributes).oldValue— the previous value, but only ifattributeOldValueorcharacterDataOldValuewas 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. oldValueis opt-in. If you readrecord.oldValuewithout enablingattributeOldValue/characterDataOldValue, it'snull.- 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.