Mutation Observer
MutationObserver lets you watch for changes in the DOM β elements added or removed, attributes modified, text content changed.
Why Use MutationObserver?
Before MutationObserver, you had to poll the DOM or use hacky DOMSubtreeModified events (deprecated, slow). MutationObserver is:
- Efficient β async, batched, no polling
- Precise β tells you exactly what changed
- Well-supported β all modern browsers
Basic Usage
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type, mutation);
});
});
observer.observe(targetElement, {
childList: true, // watch for child additions/removals
attributes: true, // watch for attribute changes
subtree: true, // watch entire subtree
characterData: true // watch text changes
});
// When done:
observer.disconnect();
Watching Child Elements
Demo: Watching Child Additions/Removals
HTML
<div id='mut-demo' style='padding:12px;border:2px solid #6366f1;border-radius:8px;'>
<p id='mut-target'>Watch list:</p>
<ul id='mut-list'>
<li>Existing item</li>
</ul>
</div>
<button id='add-mut-item'>Add Item</button>
<button id='remove-mut-item'>Remove Last</button>
<pre id='mut-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre> JavaScript
const list = document.getElementById('mut-list');
const log = document.getElementById('mut-log');
let itemCount = 1;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.type === 'childList') {
m.addedNodes.forEach(node => {
if (node.nodeType === 1) log.textContent += 'Added: ' + node.textContent + '\\n';
});
m.removedNodes.forEach(node => {
if (node.nodeType === 1) log.textContent += 'Removed: ' + node.textContent + '\\n';
});
}
});
});
observer.observe(list, { childList: true });
document.getElementById('add-mut-item').onclick = function() {
itemCount++;
const li = document.createElement('li');
li.textContent = 'Item ' + itemCount;
list.appendChild(li);
};
document.getElementById('remove-mut-item').onclick = function() {
if (list.lastElementChild) {
list.lastElementChild.remove();
}
}; Live Output Window
Watching Attribute Changes
Demo: Watching Attributes
HTML
<div id='attr-demo'>
<div id='attr-target' class='box' data-count='0' style='padding:16px;background:#eef2ff;border:2px solid #6366f1;border-radius:8px;text-align:center;'>
Watch my attributes change
</div>
<div style='margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;'>
<button id='toggle-class'>Toggle Class</button>
<button id='change-style'>Change Style</button>
<button id='inc-data'>Increment data-count</button>
</div>
<pre id='attr-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
</div> JavaScript
const target = document.getElementById('attr-target');
const log = document.getElementById('attr-log');
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.type === 'attributes') {
log.textContent =
'Attribute changed: ' + m.attributeName + '\\n' +
' oldValue: ' + m.oldValue + '\\n' +
' newValue: ' + target.getAttribute(m.attributeName);
}
});
});
observer.observe(target, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class', 'style', 'data-count'] // only watch these
});
document.getElementById('toggle-class').onclick = function() {
target.classList.toggle('active');
};
document.getElementById('change-style').onclick = function() {
target.style.background = target.style.background === '#fef9c3' ? '#eef2ff' : '#fef9c3';
};
document.getElementById('inc-data').onclick = function() {
const count = parseInt(target.dataset.count) + 1;
target.dataset.count = count;
}; Live Output Window
Watching Text Changes
Demo: Watching Text Content
HTML
<div id='text-demo'>
<p id='text-target'>Edit this text</p>
<button id='change-text'>Change Text</button>
<pre id='text-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
</div> JavaScript
const target = document.getElementById('text-target');
const log = document.getElementById('text-log');
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
if (m.type === 'characterData') {
log.textContent = 'Text changed to:' + m.target.textContent;
}
});
});
observer.observe(target, {
characterData: true,
subtree: true
});
document.getElementById('change-text').onclick = function() {
const texts = ['Hello World', 'DOM updated!', 'MutationObserver rocks', 'Edit this text'];
target.textContent = texts[Math.floor(Math.random() * texts.length)];
}; Live Output Window
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
childList | boolean | false | Watch direct child additions/removals |
attributes | boolean | false | Watch attribute changes |
characterData | boolean | false | Watch text content changes |
subtree | boolean | false | Watch entire subtree (not just direct children) |
attributeOldValue | boolean | false | Record previous attribute value |
characterDataOldValue | boolean | false | Record previous text value |
attributeFilter | string[] | all | Only watch these attributes |
MutationRecord Properties
Each mutation in the callback has:
| Property | What it contains |
|---|---|
type | "childList", "attributes", or "characterData" |
target | The node that changed |
addedNodes | Nodes added (for childList) |
removedNodes | Nodes removed (for childList) |
previousSibling / nextSibling | Sibling info (for childList) |
attributeName | Name of changed attribute |
oldValue | Previous value (if oldValue option set) |
Disconnecting
Always disconnect when youβre done:
observer.disconnect(); // stop observing
Practical: Detect Element Added
Demo: Detect New Elements
HTML
<div id='detect-demo'>
<div id='detect-container' style='padding:12px;background:#f1f5f9;border-radius:8px;min-height:60px;'>
<span class='detect-item' style='padding:4px 8px;background:#eef2ff;border-radius:4px;margin:2px;display:inline-block;'>Existing</span>
</div>
<button id='add-detect'>Add Span</button>
<pre id='detect-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
</div> JavaScript
const container = document.getElementById('detect-container');
const log = document.getElementById('detect-log');
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && node.matches('.detect-item')) {
log.textContent = 'New .detect-item added:' + node.textContent;
}
});
});
});
observer.observe(container, { childList: true });
document.getElementById('add-detect').onclick = function() {
const span = document.createElement('span');
span.className = 'detect-item';
span.style.cssText = 'padding:4px 8px;background:#c7d2fe;border-radius:4px;margin:2px;display:inline-block;';
span.textContent = 'Item ' + (container.children.length + 1);
container.appendChild(span);
}; Live Output Window
Key Takeaways
MutationObserverwatches for DOM changes β additions, removals, attributes, text- Always pass
subtree: trueif you want to watch descendants too - Use
attributeFilterto watch specific attributes only - The observer callback receives an array of MutationRecords (batched)
- Always call
.disconnect()when the observer is no longer needed (e.g., on component unmount) - Much better performance than deprecated
DOMSubtreeModifiedor polling