MutationObserver
Watches for changes in the DOM tree: added and removed elements, attribute and text changes, and reports them in a single function.
- Returns
- A new
MutationObserverobject.
Example
#const chat = document.querySelector('#chat')
const unread = document.querySelector('#unread')
const observer = new MutationObserver((mutations) => {
console.log(`Changes in this batch: ${mutations.length}`)
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => console.log('New message:', node.textContent))
}
unread.textContent = chat.children.length - 1
})
observer.observe(chat, { childList: true })
function addMessage(text) {
const item = document.createElement('li')
item.textContent = text
chat.append(item)
}
addMessage('Jack: Hey!')
addMessage('Mia: Who wants to do the quiz?')
console.log('Synchronous code finished')Both messages were added one after another, so the observer received them in a single call, only after the synchronous code finished.
Definition and usage
#MutationObserver lets you react to changes in the page’s structure, no matter which piece of code made them. It’s useful when another script, a library, or an extension changes the content and you have no control over where that happens, e.g. counting new messages in a chat or watching a widget’s attributes.
The observe(element, options) method requires you to say what to watch: childList (children added and removed), attributes (attribute changes), or characterData (text changes in text nodes). The subtree option extends the observation to all descendants, attributeFilter narrows it to selected attributes, and attributeOldValue remembers their previous values.
The callback receives an array of MutationRecord objects with fields such as type, target, addedNodes, removedNodes, and attributeName. Changes are collected and delivered together as a microtask, meaning after the current code finishes but before timers. Several changes made one after another will therefore land in a single call.
The disconnect() method ends the observation, and takeRecords() returns the changes collected so far that haven’t been delivered yet. Avoid observing the whole document with subtree: true unless you need to, because with large changes the callback will fire very often.
Syntax
#new MutationObserver(callback)
observer.observe(target, options)
observer.disconnect()
observer.takeRecords()Parameters
#callback
function (mutations, observer)
The function called after changes. It receives an array ofMutationRecordobjects.target
node
The element (or other node) the observation starts from.options
object
At least one of thechildList,attributes, orcharacterDataoptions set totrue, plus optionallysubtree,attributeFilter,attributeOldValue, andcharacterDataOldValue.
More examples
#const lesson = document.querySelector('#lesson')
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
console.log(`${mutation.attributeName}, previous value: ${mutation.oldValue}`)
}
console.log('Current status:', lesson.dataset.status)
})
observer.observe(lesson, { attributeFilter: ['data-status'], attributeOldValue: true })
lesson.dataset.status = 'in-progress'
lesson.dataset.status = 'done'
lesson.title = 'Completed'The title change doesn’t reach the observer, because attributeFilter restricts the observation to data-status. Each change gets its own entry with the value from before the change.
const editor = document.querySelector('#editor')
const note = document.querySelector('#note')
const observer = new MutationObserver((mutations) => {
console.log(mutations.map((mutation) => mutation.type))
})
observer.observe(editor, { childList: true, characterData: true, subtree: true })
note.firstChild.data = 'Note about loops'
editor.append(document.createElement('hr'))
setTimeout(() => {
observer.disconnect()
note.firstChild.data = 'Nobody will see this change'
console.log('Observation ended')
}, 100)A text change in a text node is characterData, and adding <hr> is childList. The text sits deeper than #editor, which is why the subtree option is needed. After disconnect(), further changes are no longer reported.
Browser support
#Widely available
Works in all modern browsers, phones included. You can use it without worry.
- Chrome
- Edge
- Firefox
- Safari
Good practices
#- Always say what to watch in the options. Calling
observe(element)alone, withoutchildList,attributes, orcharacterData, throws aTypeError. - Don’t change the observed fragment inside the callback without a condition. Every such change fires the observer again, and it’s easy to end up with an infinite loop.
- If you’re changing the DOM yourself in your own code, it’s simpler to call the relevant function right after the change. An observer is useful when the changes come from code you don’t control.
Related entries
#- IntersectionObserverWatches whether an element is visible within the window or a scrollable container, and calls a function whenever that changes.
- ResizeObserverCalls a function every time an observed element’s size changes, no matter the cause of the change.
- Element.append()Adds elements or text at the end of an element’s content.
- Element.setAttribute()Sets the value of an element’s HTML attribute, or adds a new attribute.
- HTMLElement.datasetAn object with an element’s data-* attribute values, for reading and storing your own data.
Spotted a mistake or missing an example? Write to us.