The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Create a mutation observer | const observer = new MutationObserver(records => process(records)) | View examples |
| Observe selected mutations | observer.observe(root, { childList: true, subtree: true }) | View examples |
| Filter attributes | observer.observe(node, { attributes: true, attributeFilter: ['aria-expanded'] }) | View examples |
| Request old values | observer.observe(node, { attributes: true, attributeOldValue: true }) | View examples |
| Drain pending records | const pending = observer.takeRecords() | View examples |
| Create an intersection observer | const observer = new IntersectionObserver(onIntersect, { threshold: [0, 0.5, 1] }) | View examples |
| Expand the root boundary | new IntersectionObserver(callback, { rootMargin: '0px 0px 300px' }) | View examples |
| Observe an intersection target | observer.observe(document.querySelector('[data-lazy]')) | View examples |
| Check visible intersection | if (entry.isIntersecting) reveal(entry.target) | View examples |
| Stop tracking one target | observer.unobserve(entry.target) | View examples |
| Drain intersection entries | const pending = observer.takeRecords() | View examples |
| Create a resize observer | const observer = new ResizeObserver(entries => update(entries)) | View examples |
| Observe the border box | observer.observe(panel, { box: 'border-box' }) | View examples |
| Read logical box size | const { inlineSize, blockSize } = entry.contentBoxSize[0] | View examples |
| Read the legacy fallback | const width = entry.contentRect.width | View examples |
| Stop observing one element | observer.unobserve(panel) | View examples |
| Disconnect an observer | observer.disconnect() | View examples |
| Feature-detect support | if ('ResizeObserver' in globalThis) startResizeTracking() | View examples |
Observer APIs report changes asynchronously without continuous polling: MutationObserver watches DOM structure and data, IntersectionObserver tracks geometric threshold crossings, and ResizeObserver reports element box-size changes. They solve different problems and deliver at different points in the event loop or rendering cycle. Reliable use depends on observing the narrowest target, selecting only necessary data, keeping callbacks bounded, preventing feedback loops, and disconnecting observers when their owner is destroyed.
Step by step
Detailed examples
Configure MutationObserver for the smallest useful change set
observe requires at least one of childList, attributes, or characterData to be true. subtree extends those choices through descendants; it does not mean every mutation type is enabled. attributeFilter avoids noisy unrelated records, and observing a stable container is usually cheaper and more reliable than creating an observer for every child. Reusing observe on the same observer and target updates that registered observation. Mutation observation reports DOM changes, not layout, painted pixels, input values stored only as properties, or every semantic state change.
<section id="results" aria-busy="false"></section>
<script>
const results = document.querySelector('#results');
const observer = new MutationObserver(records => {
for (const record of records) {
if (record.type === 'childList') {
console.log('Added nodes:', record.addedNodes.length);
} else {
console.log(record.attributeName, record.oldValue, results.getAttribute(record.attributeName));
}
}
});
observer.observe(results, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-busy'],
attributeOldValue: true
});
</script> Process mutation batches without causing recursive churn
Mutation records are queued and callbacks run asynchronously at the mutation-observer microtask checkpoint, after the current JavaScript stack rather than once per DOM operation. A record describes a mutation; it is not a complete before-and-after snapshot. attributeOldValue and characterDataOldValue retain requested previous strings at additional cost. Changes made by the callback can queue another delivery, so separate reads from writes and ignore mutations your own code owns. takeRecords synchronously drains queued records. disconnect stops all registrations and empties the queue, so call takeRecords first when final pending records matter.
function handleMutations(records) {
const added = records.flatMap(record => [...record.addedNodes]);
for (const node of added) {
if (node.nodeType === Node.ELEMENT_NODE) indexElement(node);
}
}
const observer = new MutationObserver(handleMutations);
observer.observe(container, { childList: true, subtree: true });
function destroy() {
handleMutations(observer.takeRecords());
observer.disconnect();
} Define intersection geometry with roots, margins, and thresholds
With root omitted or null, the target is measured against the top-level viewport. For useful intersection with a non-null Element root, the target must be its descendant in the containing-block chain. rootMargin expands or contracts the root intersection rectangle using CSS-like offsets, while threshold values from 0 through 1 identify ratio crossings and are sorted by the user agent. Callbacks are asynchronous and may coalesce intermediate geometry, so this API is not a pixel-perfect scroll-event replacement. Occlusion by another element, opacity, filters, and visual effects are not reflected by the default intersection calculation.
const preloadObserver = new IntersectionObserver(entries => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
preloadCard(entry.target);
preloadObserver.unobserve(entry.target);
}
}, {
root: document.querySelector('.catalog-scrollport'),
rootMargin: '0px 0px 240px',
threshold: 0
});
document.querySelectorAll('[data-preload]').forEach(card => {
preloadObserver.observe(card);
}); Use intersection signals as hints, not proof of attention
isIntersecting records whether the target and root intersect; intersectionRatio is the intersecting area divided by the target area, with special handling for zero-area targets. An initial entry is queued after observation begins, and later entries arrive when threshold state changes. This makes the API suitable for lazy work, pagination sentinels, and approximate exposure timing. It does not prove that a human saw an element. Background tabs can delay delivery, cross-origin targets restrict root geometry and margin effects, and accessibility must never depend on intersection alone. Native loading=lazy remains preferable for ordinary images and frames.
const campaign = document.querySelector('[data-campaign]');
let intersectsEnough = false;
let startedAt = null;
let visibleMs = 0;
function reconcileExposure() {
const now = performance.now();
const shouldCount = intersectsEnough && document.visibilityState === 'visible';
if (startedAt !== null && !shouldCount) {
visibleMs += now - startedAt;
startedAt = null;
} else if (startedAt === null && shouldCount) {
startedAt = now;
}
}
const exposure = new IntersectionObserver(([entry]) => {
intersectsEnough = entry.isIntersecting && entry.intersectionRatio >= 0.5;
reconcileExposure();
}, { threshold: 0.5 });
document.addEventListener('visibilitychange', reconcileExposure);
exposure.observe(campaign); Observe the box whose dimensions drive the component
ResizeObserver reports an element's content, border, or device-pixel content box without polling the viewport. contentBoxSize and borderBoxSize expose logical inline and block dimensions and can contain multiple fragments; current layouts commonly return one entry, but code should choose deliberately. contentRect is the older content-box fallback. device-pixel-content-box is useful for sharp canvas backing stores but needs feature detection. CSS transforms do not change the observed box size, and observing an inline element with no generated box will not provide useful element-resize behavior.
const panel = document.querySelector('.summary-panel');
const observer = new ResizeObserver(([entry]) => {
const size = entry.borderBoxSize?.[0];
const width = size ? size.inlineSize : entry.contentRect.width;
panel.classList.toggle('summary-panel--compact', width < 420);
});
observer.observe(panel, { box: 'border-box' }); Bound callback work and own the observer lifecycle
Resize notifications run after layout and before paint. Writing a size that triggers another resize can create a notification loop; the browser limits delivery and reports a ResizeObserver loop error while deferring remaining notifications. Prefer CSS container queries for purely visual breakpoints, cache the last applied state, and schedule nonessential writes in requestAnimationFrame. IntersectionObserver and ResizeObserver can unobserve one target; MutationObserver cannot. MutationObserver and IntersectionObserver expose takeRecords, while ResizeObserver does not. All three support disconnect for complete teardown. Feature detection should preserve core content and behavior rather than hiding it.
function mountMeter(meter) {
let frame = 0;
let pendingWidth = 0;
const observer = new ResizeObserver(([entry]) => {
pendingWidth = entry.contentRect.width;
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
meter.style.setProperty('--measured-width', `${pendingWidth}px`);
});
});
observer.observe(meter);
return () => {
observer.disconnect();
if (frame) cancelAnimationFrame(frame);
};
} Local code tester
Watch DOM additions and element resizing
Add cards to trigger MutationObserver, then resize the container to trigger ResizeObserver without any network or storage access.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



