Chrome Extension Dynamic Content Injection — Best Practices
5 min readDynamic Content Injection
Dynamic content injection enables Chrome Extensions to inject floating widgets, toolbars, sidebars, and other UI elements directly into web pages from content scripts. This pattern is fundamental for creating rich, interactive extensions that integrate seamlessly with host pages.
Overview
Content scripts can inject UI elements into pages, but doing so safely requires careful consideration of:
- Style isolation from page CSS
- Positioning and z-index management
- Page navigation handling
- Clean removal on extension disable
See also: Content Script Isolation, Shadow DOM Advanced, Content Script Patterns
Floating Widget Pattern
Position Fixed Overlay
Floating widgets use position: fixed to remain relative to the viewport:
// Inject a floating action button
const widget = document.createElement('div');
widget.id = 'my-extension-fab';
widget.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
width: 56px;
height: 56px;
border-radius: 50%;
background: #6366f1;
color: white;
cursor: pointer;
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
widget.textContent = '+';
document.body.appendChild(widget);
Draggable Panel
function makeDraggable(element: HTMLElement): void {
let isDragging = false;
let offsetX = 0, offsetY = 0;
element.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - element.getBoundingClientRect().left;
offsetY = e.clientY - element.getBoundingClientRect().top;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
element.style.left = `${e.clientX - offsetX}px`;
element.style.top = `${e.clientY - offsetY}px`;
});
document.addEventListener('mouseup', () => isDragging = false);
}
Shadow DOM for Style Isolation
Shadow DOM prevents page CSS from affecting your injected UI:
function createIsolatedWidget(): HTMLElement {
const container = document.createElement('div');
container.id = 'my-extension-widget';
const shadow = container.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host {
position: fixed;
top: 20px;
right: 20px;
font-family: system-ui, sans-serif;
z-index: 999999;
}
.widget {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
.widget { background: #1e1e1e; color: #fff; }
}
`;
const widget = document.createElement('div');
widget.className = 'widget';
widget.textContent = 'Hello from extension!';
shadow.appendChild(style);
shadow.appendChild(widget);
document.body.appendChild(container);
return container;
}
CSS Injection Methods
chrome.scripting.insertCSS (Manifest V3)
await chrome.scripting.insertCSS({
target: { tabId: tab.id },
css: '.my-extension-class { color: red; }'
});
Style Element Creation
For dynamic styles with page-specific logic:
function injectStyles(css: string): HTMLStyleElement {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
return style;
}
Use chrome.scripting for static CSS; use style elements for dynamic, runtime-generated styles.
Z-Index Management
Chrome extensions should use z-index values in the safe range:
| Value Range | Usage |
|---|---|
| 0 - 9999 | Page content |
| 10000000+ | Browser chrome (address bar, etc.) |
| 2147483647 | Maximum safe value for page elements |
const Z_INDEX_SAFE = 2147483640; // Leave room for edge cases
element.style.zIndex = String(Z_INDEX_SAFE);
Handling Page Navigation
Single-page applications (SPAs) don’t trigger page loads. Use the navigation API or polling:
// Using chrome.webNavigation (requires permission)
chrome.webNavigation.onCompleted.addListener(() => {
initializeExtension();
});
// Fallback: MutationObserver for SPAs
const observer = new MutationObserver(() => {
if (!document.getElementById('my-extension-widget')) {
createIsolatedWidget();
}
});
observer.observe(document.body, { childList: true, subtree: true });
Clean Removal
Remove all injected content when the extension disables or the script unloads:
function cleanup(): void {
const elements = document.querySelectorAll('[id^="my-extension-"]');
elements.forEach(el => el.remove());
}
// Listen for extension disable
chrome.runtime.onSuspend.addListener(cleanup);
window.addEventListener('unload', cleanup);
When to Use iframe vs Direct DOM
| Approach | Use Case |
|---|---|
| Direct DOM | Floating buttons, toolbars, sidebars - full page integration |
| iframe | Isolated third-party content, complex iframe-friendly libraries |
For most extension UI, direct DOM with Shadow DOM provides the best balance of integration and isolation. -e —
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.