Skip to main content

How to Detect When an Element is Added to or Removed from the DOM in JavaScript

In dynamic web applications, you sometimes need to run code in response to an element being added to or removed from the DOM. This is common when working with third-party libraries that manipulate the DOM, or when you need to trigger animations or cleanup logic. The modern, standard tool for this is the MutationObserver API.

This guide will teach you how to use MutationObserver to reliably detect when child nodes are added to or removed from a target element. You will learn the basic setup and how to inspect the mutation records to take specific actions.

The Core Tool: The MutationObserver API

The MutationObserver is a built-in browser API that provides a way to react to changes in the DOM. It is the modern and performant replacement for the older, deprecated Mutation Events.

The logic:

  1. Create an Observer: You create a new MutationObserver instance, passing it a callback function that will be executed whenever a DOM change is detected.
  2. Define a Target: You choose a DOM element that you want to watch for changes.
  3. Start Observing: You call the .observe() method on your observer, telling it which element to watch and what kind of changes to look for (e.g., additions/removals of children, attribute changes, etc.).

How to Detect When an Element is Added

This is the most common use case. You want to run code as soon as a specific element appears in the DOM.

You have a container, and you want to know when a new element with the class my-widget is added inside it.

HTML:

<div id="container">
<!-- New elements will be added here -->
</div>

Solution:

const container = document.getElementById('container');

// 1. Create the observer
const observer = new MutationObserver((mutationsList) => {
// Loop through all mutations that just occurred
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// Check the nodes that were added
mutation.addedNodes.forEach(node => {
// We must check if the node is an Element because
// text nodes can also be added
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('my-widget')) {
console.log('A .my-widget element was added!');
// You can run your initialization logic here
}
});
}
}
});

// 2. Start observing the container for changes
observer.observe(container, {
childList: true, // Watch for the addition or removal of child nodes
subtree: true, // Watch the entire subtree, not just direct children
});

// --- To test it, add an element after a delay ---
setTimeout(() => {
const newWidget = document.createElement('div');
newWidget.className = 'my-widget';
newWidget.textContent = 'I am the new widget!';
container.appendChild(newWidget);
}, 2000);

How to Detect When an Element is Removed

The process for detecting removed elements is nearly identical. Instead of checking the mutation.addedNodes property, you check the mutation.removedNodes property.

Solution:

const container = document.getElementById('container');
const elementToRemove = container.querySelector('.my-widget');

const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// Check the nodes that were removed
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('my-widget')) {
console.log('A .my-widget element was removed!');
// You can run your cleanup logic here

// If you only need to detect this once, you can stop observing
observer.disconnect();
}
});
}
}
});

observer.observe(container, { childList: true, subtree: true });

// --- To test it, remove the element after a delay ---
setTimeout(() => {
if (elementToRemove) {
elementToRemove.remove();
}
}, 2000);

Key Configuration Options (childList and subtree)

When you call observer.observe(), the second argument is a configuration object. The two most important properties for this task are:

  • childList: true: This is required. It tells the observer to watch for nodes being added to or removed from the target's direct children.
  • subtree: true: This is highly recommended. It tells the observer to watch for changes not only in the direct children of the target but in all of its descendants, no matter how deeply nested they are. Without this, you would only be notified if an element was added directly to the container you are watching.

Practical Example: Initializing a Third-Party Widget

A perfect use case for MutationObserver is when you are working with a third-party script that adds an element to your page, and you need to run your own code on that element as soon as it exists.

// Imagine a third-party ad script will eventually add a div with the
// class 'ad-banner' somewhere inside the <body>.

const observer = new MutationObserver((mutationsList, obs) => {
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.matches('.ad-banner')) {
console.log('Ad banner has been added to the page. Attaching custom styles...');

// Now that the element exists, we can work with it
node.style.border = '2px solid red';

// We found what we were looking for, so we can stop observing
obs.disconnect();
return;
}
}
}
});

// Start observing the entire document body for changes.
observer.observe(document.body, { childList: true, subtree: true });

Conclusion

The MutationObserver API is the modern, performant, and correct way to react to elements being added to or removed from the DOM.

  • To detect changes, create a new MutationObserver(callback).
  • Use observer.observe(targetNode, { childList: true, subtree: true }) to start watching a target element and its entire contents.
  • Inside the callback, inspect the mutation.addedNodes and mutation.removedNodes properties to see which elements have changed.
  • Always check that a node is an actual element (node.nodeType === Node.ELEMENT_NODE) before trying to access properties like .classList or .matches().

By using MutationObserver, you can write robust, event-driven code that responds reliably to a dynamic DOM.