JavaScriptBrowser APIs

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 MutationObserver object.
On this page

Example

#
JavaScript
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')
Console
Synchronous code finished
Changes in this batch: 2
New message: Jack: Hey!
New message: Mia: Who wants to do the quiz?

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

#
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 of MutationRecord objects.
  • target

    node

    The element (or other node) the observation starts from.
  • options

    object

    At least one of the childList, attributes, or characterData options set to true, plus optionally subtree, attributeFilter, attributeOldValue, and characterDataOldValue.

More examples

#
Attribute change with the previous value
JavaScript
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'
Console
data-status, previous value: todo
data-status, previous value: in-progress
Current status: done

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.

An entire subtree and disconnect()
JavaScript
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)
Console
['characterData', 'childList']
Observation ended

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, without childList, attributes, or characterData, throws a TypeError.
  • 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

#

Spotted a mistake or missing an example? Write to us.