New:Embedded & IoT Bootcamp
Tutorial 1 of 3 in series

Mutation Observer

Watch for DOM changes with MutationObserver - additions, removals, and attribute changes

Abhishek Sharma 2026-06-10 Web Development / Frontend / JavaScript DOM / Misc
33%

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

  • MutationObserver watches for DOM changes - additions, removals, attributes, text
  • Always pass subtree: true if you want to watch descendants too
  • Use attributeFilter to 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 DOMSubtreeModified or polling

Keep learning

Related tutorials from across the library.

All tutorials
Tutorial2026-06-10

Selection and Range

Work with text selection and ranges in the DOM

Web Development / Frontend / JavaScript DOM / Misc

JavaScriptDOM
Tutorial2026-06-10

Event Loop

Understand the JavaScript event loop - microtasks, macrotasks, and async execution

Web Development / Frontend / JavaScript DOM / Misc

JavaScriptDOM
Tutorial2026-06-10

Browser Environment & Specs

Understand the JavaScript browser environment - window, DOM, BOM, and ECMAScript

Web Development / Frontend / JavaScript DOM / Document

JavaScriptDOM