The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a mutation observerconst observer = new MutationObserver(records => process(records))View examples
Observe selected mutationsobserver.observe(root, { childList: true, subtree: true })View examples
Filter attributesobserver.observe(node, { attributes: true, attributeFilter: ['aria-expanded'] })View examples
Request old valuesobserver.observe(node, { attributes: true, attributeOldValue: true })View examples
Drain pending recordsconst pending = observer.takeRecords()View examples
Create an intersection observerconst observer = new IntersectionObserver(onIntersect, { threshold: [0, 0.5, 1] })View examples
Expand the root boundarynew IntersectionObserver(callback, { rootMargin: '0px 0px 300px' })View examples
Observe an intersection targetobserver.observe(document.querySelector('[data-lazy]'))View examples
Check visible intersectionif (entry.isIntersecting) reveal(entry.target)View examples
Stop tracking one targetobserver.unobserve(entry.target)View examples
Drain intersection entriesconst pending = observer.takeRecords()View examples
Create a resize observerconst observer = new ResizeObserver(entries => update(entries))View examples
Observe the border boxobserver.observe(panel, { box: 'border-box' })View examples
Read logical box sizeconst { inlineSize, blockSize } = entry.contentBoxSize[0]View examples
Read the legacy fallbackconst width = entry.contentRect.widthView examples
Stop observing one elementobserver.unobserve(panel)View examples
Disconnect an observerobserver.disconnect()View examples
Feature-detect supportif ('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

01

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.

Track inserted results and one state attribute
<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>
Back to quick reference ↑
02

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.

Drain pending records during component teardown
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();
}
Back to quick reference ↑
03

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.

Build one observer for staged preloading
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);
});
Back to quick reference ↑
04

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.

Accumulate approximate visible time with page visibility
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);
Back to quick reference ↑
05

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.

Switch a component using its own border-box width
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' });
Back to quick reference ↑
06

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.

Schedule resize-driven writes and clean up
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);
  };
}
Back to quick reference ↑

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.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGDOM Standard: Mutation observersdom.spec.whatwg.org
  2. World Wide Web ConsortiumIntersection Observerw3.org
  3. World Wide Web ConsortiumResize Observerw3.org
  4. WHATWGHTML Standard: Update the renderinghtml.spec.whatwg.org
  5. World Wide Web ConsortiumCSS Containment Module Level 3w3.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback