MutationObserver watches part of the DOM and tells you when something changes. It can detect added elements, removed elements, text changes, and attribute changes without repeatedly scanning the whole page.
Modern pages often change after the first load. Comments appear, widgets inject markup, forms update, ads load, menus open, and JavaScript adds new blocks. MutationObserver gives you a clean way to react when the page structure changes.
This example watches a container and logs whenever child elements are added or removed.
<div id="watchArea">
<p>Watch this area</p>
</div>
<script>
const watchArea = document.getElementById('watchArea');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
console.log('DOM changed:', mutation);
}
});
});
observer.observe(watchArea, {
childList: true
});
</script>
A common use is detecting when new elements are added to a feed, comment list, tool result area, or widget container.
<div id="commentList"></div>
<script>
const commentList = document.getElementById('commentList');
const commentObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== 1) return;
if (node.matches('.comment')) {
node.classList.add('is-new');
}
});
});
});
commentObserver.observe(commentList, {
childList: true
});
</script>
Use subtree:true when you want to watch changes inside all nested elements, not just direct children.
<main id="pageContent">
<section>
<p>Nested content can change here.</p>
</section>
</main>
<script>
const pageContent = document.getElementById('pageContent');
const pageObserver = new MutationObserver((mutations) => {
console.log('Something changed inside pageContent');
});
pageObserver.observe(pageContent, {
childList: true,
subtree: true
});
</script>
MutationObserver can also watch attributes like class, hidden, aria-expanded, or data-* values.
<button id="menuButton" aria-expanded="false">
Menu
</button>
<script>
const button = document.getElementById('menuButton');
const attributeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'aria-expanded') {
console.log('Menu state changed:', button.getAttribute('aria-expanded'));
}
});
});
attributeObserver.observe(button, {
attributes: true,
attributeFilter: ['aria-expanded']
});
</script>
You can watch text updates too. This is useful for counters, status messages, live result areas, and dynamic labels.
<div id="statusText">Waiting...</div>
<script>
const statusText = document.getElementById('statusText');
const textObserver = new MutationObserver(() => {
console.log('Status changed:', statusText.textContent);
});
textObserver.observe(statusText, {
childList: true,
characterData: true,
subtree: true
});
</script>
If you only need to watch temporarily, call disconnect() when you are done. This helps avoid unnecessary work.
const observer = new MutationObserver((mutations) => {
console.log('Change detected');
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Stop observing later
observer.disconnect();
Detecting injected widgets, styling newly added comments, watching live result areas, reacting to class changes, monitoring accessibility attributes, handling dynamic form updates, watching third-party embeds, and responding when JavaScript adds or removes page elements.
Watching the entire document when a smaller container would work, using subtree:true everywhere without a reason, doing expensive work inside every callback, forgetting to call disconnect() when finished, or using MutationObserver for things that should be handled directly in your own code.
Normal events are better when you control the action, like a button click or form submit. MutationObserver is better when you need to react to DOM changes that may happen indirectly, especially from dynamic scripts, widgets, async updates, or third-party code.
Polling checks again and again on a timer. MutationObserver waits for the browser to report actual DOM changes. For many cases, that is cleaner and lighter than running repeated checks with setInterval().
MutationObserver watches DOM changes. ResizeObserver watches size changes. Intersection Observer watches whether something is visible in the viewport or inside a scroll container.
If you want to detect when elements enter or leave the viewport, read the Intersection Observer guide. If you want to detect when elements change size, read the ResizeObserver guide.
Use MutationObserver when you need to know that the page structure changed after load. It is especially helpful when content is created by JavaScript, loaded with AJAX, injected by a widget, or changed by third-party scripts.
It gives you a native browser feature for reacting to DOM changes without a framework. For VibeScriptz-style pages and tools, MutationObserver is useful because it solves real dynamic-page problems with small, plain JavaScript.
Read the full API reference on MDN Web Docs.
CSS container queries, content visibility, lazy loading, infinite scroll, event delegation, dynamic forms, and DOM performance would all connect well to this topic.