JavaScript

MutationObserver: the tool you need when third-party scripts inject your HTML

A JavaScript observer detects third-party injected HTML and attaches an event listener.

Sometimes the element you need does not exist when your script runs.

That happens often with third-party tools:

  • warranty modals
  • chat widgets
  • review widgets
  • financing buttons
  • app-injected Shopify blocks
  • tracking consent banners
  • lead forms

The page loads. Your JavaScript runs. You query the DOM. The element is not there yet. A few seconds later, a third-party script injects the HTML and your event listener never attaches.

This is exactly the kind of problem MutationObserver solves.

The wrong fix: arbitrary timeouts

The quick fix is usually this:

setTimeout(() => {
  document.querySelector("[data-modal-trigger]").addEventListener("click", openModal);
}, 2000);

That might work on your machine. It will fail when:

  • the third-party script loads slower
  • the network is delayed
  • the user interacts early
  • the app changes injection timing
  • the target element is replaced later

Timeouts guess. Observers watch.

The better pattern

Use MutationObserver to watch for the element.

const attachModalListener = () => {
  const trigger = document.querySelector("[data-modal-trigger]");

  if (!trigger || trigger.dataset.listenerAttached === "true") {
    return false;
  }

  trigger.addEventListener("click", () => {
    document.body.classList.add("modal-open");
  });

  trigger.dataset.listenerAttached = "true";
  return true;
};

if (!attachModalListener()) {
  const observer = new MutationObserver(() => {
    if (attachModalListener()) {
      observer.disconnect();
    }
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });
}

This pattern does four important things:

  1. It tries immediately.
  2. It watches for later DOM changes.
  3. It avoids attaching duplicate listeners.
  4. It disconnects once the job is done.

When to keep observing

Sometimes you should not disconnect.

Keep observing when:

  • the third-party app repeatedly replaces the element
  • product forms re-render after variant changes
  • filters rebuild the product grid
  • cart drawers replace internal markup
  • SPA-like navigation changes page sections

In those cases, make your listener attachment idempotent. The code should be safe to run many times.

Event delegation can be even simpler

If the click bubbles, event delegation may be cleaner:

document.addEventListener("click", (event) => {
  const trigger = event.target.closest("[data-modal-trigger]");

  if (!trigger) return;

  document.body.classList.add("modal-open");
});

This works even if the element is added later, because the listener is attached to document.

Use delegation when possible. Use MutationObserver when you need to react to the element appearing, read injected data, initialize a widget, or modify the injected markup.

Practical debugging checklist

When a listener does not fire:

  1. Confirm the element exists when your script runs.
  2. Confirm the element is not replaced after your listener attaches.
  3. Check whether the interaction happens inside an iframe.
  4. Check whether the third-party script stops propagation.
  5. Try event delegation.
  6. Use MutationObserver when the DOM is injected or replaced.

The key is to stop assuming page load means DOM complete. On modern websites, especially Shopify and WordPress sites with third-party tools, the DOM keeps changing.

MutationObserver gives you a clean way to work with that reality.