Injecting Custom UI Into a Website You Don't Own

TIL that a naive injected panel kept getting wiped out by the host page's own re-renders — the fix was watching for that, not fighting it, and mounting into a container the host page has no reason to touch.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

The extension needed to add a delivery-management panel directly inside the POS's own interface. The first version appended a div into an existing container on the page — which worked until the POS's own JavaScript re-rendered that container and wiped my injected panel out along with it.

Context #

The POS is a single-page app with its own client-side rendering, meaning parts of the DOM I don't control get replaced wholesale, not just updated in place, whenever its own state changes.

What I Tried #

Re-injecting the panel on a timer, on the theory that even if it got removed, it would reappear shortly after.

What Went Wrong #

This produced a visible flicker every time the host page re-rendered, and occasionally a race where my re-injection ran mid-render and got removed again immediately — a bad user experience dressed up as a fix.

The Solution #

Stopped injecting into a container the host page owned and instead created a dedicated top-level container appended directly to document.body, mounted a React root into it, and used a MutationObserver to detect if that container itself ever got removed (rare, but possible on full page navigations within the SPA) rather than polling on a timer.

const container = document.createElement("div");
container.id = "delivery-ext-root";
document.body.appendChild(container);
const root = createRoot(container);
root.render(<DeliveryPanel />);
 
new MutationObserver(() => {
  if (!document.body.contains(container)) document.body.appendChild(container);
}).observe(document.body, { childList: true });

Why It Works #

A container appended directly to document.body, outside any element the host app's own render logic manages, has no reason to be touched by that app's re-renders — it's simply not part of the tree the SPA framework is diffing. The MutationObserver handles the rare case where something removes it, reactively instead of on a fixed poll.

Lessons Learned #

Injecting into someone else's DOM tree means inheriting their re-render behavior unless you deliberately opt out of it by mounting somewhere they don't manage. Polling was treating a symptom; picking a mount point outside their ownership fixed the actual cause.

What I Would Do Differently #

I'd default to a body-level, framework-agnostic mount point for any injected UI from the start, rather than the more "convenient" approach of reusing an existing container on the page.

Shadow DOM isolation for injected UI, MutationObserver, SPA re-render behavior.