The Surprising Complexity of Injecting a UI into Someone Else's Web Page Parvej Shah's team at Leadswave, a LinkedIn brand assistant Chrome extension, overcame significant technical hurdles including style collisions, service worker lifecycle issues, and Manifest V3 constraints. They used Shadow DOM for style isolation and direct API calls from content scripts to handle service worker termination, while also optimizing AI context extraction to reduce token usage. Originally published at parvejshah.com/blog/building-manifest-v3-ai-chrome-extensions by Parvej Shah . When we started building Leadswave — a LinkedIn brand assistant Chrome extension — the plan seemed straightforward. Detect LinkedIn post elements in the feed, attach a small AI companion button to each one, let users generate engagement responses without leaving the page. A week's work, maybe two. Three weeks later we were still fighting style collisions, service worker lifecycle bugs, and a Manifest V3 API constraint we hadn't anticipated. This is what we learned. When your content script injects HTML into LinkedIn's document, you're working inside a page you don't control, with styles you didn't write, against a DOM structure that changes in LinkedIn's deployments without your knowledge. The first version of Leadswave simply appended a styled div to each post element and mounted a React component inside it. It worked locally. On the actual LinkedIn feed, our Tailwind utility classes either had no effect because LinkedIn's CSS specificity was higher or caused unintended effects in LinkedIn's own components because our styles bled into their DOM nodes . The fix is Shadow DOM. Every modern browser supports the ability to attach a shadow root to a host element, creating an isolated DOM subtree that is completely separate from the main document's style cascade. js function mountAssistantWidget hostElement: HTMLElement : HTMLElement { const container = document.createElement "div" ; container.id = "lw-assistant-root"; // Attach a shadow root — this creates the style isolation boundary const shadow = container.attachShadow { mode: "open" } ; // Our styles live inside the shadow — they can't bleed out, // and LinkedIn's styles can't bleed in const styleSheet = document.createElement "link" ; styleSheet.rel = "stylesheet"; styleSheet.href = chrome.runtime.getURL "content/styles.css" ; shadow.appendChild styleSheet ; const mountPoint = document.createElement "div" ; shadow.appendChild mountPoint ; hostElement.appendChild container ; return mountPoint; } Manifest V2 allowed persistent background pages — JavaScript modules that stayed alive indefinitely and could hold state in memory. Manifest V3 replaced this with service workers. Service workers can be terminated by the browser at any point when they appear idle. This is easy to forget when developing locally, because Chrome is less aggressive about terminating service workers during active development. In production, with a real user who opens LinkedIn once, reads through their feed over 20 minutes, and then triggers the assistant widget — the service worker has almost certainly been terminated in the interim. // Don't rely on module-level variables for persistent state // This will be undefined after the service worker restarts: // let cachedApiKey: string | null = null; // BAD // Instead, always read from storage: async function getApiKey : Promise