JavaScript / Interactive field guide

Something changed.
How does your code know?

Explore five ways to react to DOM changes. Read the code, change the page, and watch what fires.

Based on “Let’s Talk About Detecting, Monitoring, and Using DOM Changes in JavaScript” by zhangxinxu. Examples adapted for this page.

01 / Let the component respond

Custom element lifecycle callbacks

A custom <x-ell> watches its rows attribute and updates its line clamp. Change the property, or detach and reconnect the same element to see its lifecycle.

Code:
Result:
The DOM is a living tree. JavaScript can add branches, replace text, and change attributes while a page is open. Custom elements give each component a place to respond to its own lifecycle. Change the rows attribute and this paragraph updates its presentation automatically. The full text remains in the DOM; CSS controls how many lines you see. Small, explicit callbacks keep the component’s behavior close to the element that owns it.

Only attributes listed in observedAttributes trigger attributeChangedCallback. Adoption into another document is included in the code but is not triggered by these controls.

02 / Observe an existing node

MutationObserver: collect changes, then react

Watch attributes, text-node edits, and child replacements. Several synchronous mutations can arrive in one callback. This example observes a plain element so it stays independent of the custom element above.

Code:
Result:
MutationObserver watches a selected part of the DOM. It can report attribute changes, additions and removals of child nodes, and changes to text nodes. Its callback runs asynchronously after synchronous JavaScript finishes. Several mutations can be delivered together, letting your code respond once to a group of changes. Try the controls to inspect the records and change the line count of this paragraph.

Editing Text.data produces characterData; assigning textContent replaces children and produces childList. The callback renders the owning element because a text mutation’s target is a Text node.

03 / Understand the historical approach

Mutation events Legacy · simulation

Legacy mutation events ran synchronously. They are obsolete and should not be used for new implementations. This explicitly labeled simulation uses a custom event to illustrate removal timing, alongside a real MutationObserver.

Code:
Result:
A removable node

The custom “demo:before-remove” event is manually dispatched; it is not native DOMNodeRemoved detection. The observer receives an empty NodeList for additions and a NodeList containing the removed node.

04 / Intercept an explicit assignment

Object.defineProperty: a setter with a side effect

A setter can render immediately when you assign element.rows. It does not observe arbitrary DOM changes: setting the attribute directly bypasses the setter. Try both routes.

Code:
Result:
A property setter is a small, direct contract between your code and an object. Assigning to rows calls the setter, updates the attribute, and renders immediately. But a separate call to setAttribute does not pass through this property setter. That distinction matters when other code can change the element. Use the buttons to compare the stored attribute with the number of lines last applied by the rendering function. This paragraph contains enough text to make the difference visible.

Accessor descriptors use get and set. Do not include writable with an accessor descriptor; “writeable” from the original snippet is not a recognized descriptor key.

05 / Detect a CSS animation signal

CSS animation as a selector-based signal

New matching nodes receive a short animation. A delegated animationend listener detects its completion. This is an indirect signal, not a general-purpose observer: it provides no mutation records and cannot reliably report arbitrary removals.

Code:
Result:

This imperceptible animation changes no layout. Removing a node before its animation completes can prevent animationend from firing.