Skip to content
portfolioblogtaming-webflow-with-mutationobserver
cd ../blog

18 May 2026 · 2 min read

Taming dynamic Webflow content with MutationObserver

WebflowJavaScriptDOM

If you write custom JavaScript for Webflow sites long enough, you'll hit the same wall I did: your script runs on page load, decorates the DOM perfectly — and then a CMS filter, a pagination click or a third-party embed re-renders everything and wipes your work out.

You can fight this with setTimeout and hope. Or you can let the browser tell you exactly when the DOM changes.

The problem in one example

Say each CMS item carries a status in a hidden text element, and you want to apply a matching badge class:

naive-version.js
document.querySelectorAll("[data-status]").forEach((el) => {
  el.classList.add(`badge--${el.dataset.status.toLowerCase()}`);
});

Works on load. Breaks the moment a filtering library swaps the list out, because the new nodes never ran through your loop.

Observe the container, not the items

The fix is to watch the list container and re-apply your logic whenever children change:

status-badges.js
function applyBadges(root) {
  root.querySelectorAll("[data-status]:not([data-badged])").forEach((el) => {
    const status = el.dataset.status?.trim().toLowerCase();
    if (!status) return;
    el.classList.add(`badge--${status}`);
    el.dataset.badged = "true"; // idempotency guard
  });
}
 
const list = document.querySelector('[data-list="properties"]');
if (list) {
  applyBadges(list);
 
  const observer = new MutationObserver(() => applyBadges(list));
  observer.observe(list, { childList: true, subtree: true });
}

Three details matter more than they look:

  1. Idempotency. The observer fires often, sometimes for changes you caused yourself. The data-badged guard means re-running is free and you never double-apply classes.
  2. Scope the observer. Observing document.body with subtree: true works, but you'll process every tooltip and animation frame on the page. Watch the smallest container that contains your content.
  3. Don't mutate inside the callback carelessly. If your handler changes the same subtree it observes, guard against infinite loops — the idempotency check above does double duty here.

Debounce when the work is heavy

For cheap class toggles, running on every mutation is fine. If your handler measures layout or fetches data, batch the bursts:

let scheduled = false;
const observer = new MutationObserver(() => {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    scheduled = false;
    applyBadges(list);
  });
});

A requestAnimationFrame debounce collapses a filter re-render's dozens of mutations into one pass, right before paint.

When not to use it

If the library re-rendering your DOM exposes events or callbacks — Finsweet's attributes solutions do, for example — prefer those. They fire once, with intent, at the right moment. MutationObserver is the fallback for everything that doesn't tell you when it's done.

That said, as a defensive pattern it's hard to beat: it survives library version bumps, editor re-publishes and embeds you don't control. On client sites, that durability is worth a lot.

// thanks for reading

Questions, corrections or war stories of your own? Email me — I read everything.