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.
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.
Related Concepts #
Shadow DOM isolation for injected UI, MutationObserver, SPA re-render behavior.
Related content
Writing a Reliable Regex for Extracting Delivery Times
TIL that a regex tuned against a handful of examples ('DELIVERY TIME: 2:00 PM') broke on real data within a day because of extra whitespace and inconsistent AM/PM casing I hadn't accounted for.
Extracting Delivery Information from Dynamically Rendered HTML
TIL that a content script querying the DOM immediately on page load found nothing, because the POS's order details render client-side, after the content script's own 'document_idle' point had already passed.
Reading Data from an Existing Web App with a Chrome Content Script
TIL that a content script reading order data straight from the DOM breaks every time the POS ships a markup change — the fix was reading from the most stable layer available, not the most convenient one.