npx claudepluginhub arustydev/agents --plugin browser-extension-devWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
Browser API differences, polyfills, and feature detection for Firefox, Chrome, Safari, and Edge extensions
This skill uses the workspace's default tool permissions.
Cross-Browser Extension Compatibility
Comprehensive guide to writing browser extensions that work across Chrome, Firefox, Safari, and Edge with proper feature detection and polyfills.
Overview
Browser extensions share a common WebExtensions API standard, but implementations differ significantly. This skill covers how to handle those differences.
This skill covers:
- API compatibility matrices
- Polyfill usage and patterns
- Feature detection techniques
- Browser-specific workarounds
- Manifest differences
This skill does NOT cover:
- General JavaScript compatibility (use caniuse.com)
- Extension store submission (see
extension-anti-patternsskill) - UI framework differences
Quick Reference
Browser API Namespaces
| Browser | Namespace | Promises | Polyfill Needed |
|---|---|---|---|
| Chrome | chrome.* | Callbacks | Yes |
| Firefox | browser.* | Native | No |
| Safari | browser.* | Native | No |
| Edge | chrome.* | Callbacks | Yes |
Universal Pattern
// Use webextension-polyfill for consistent API
import browser from 'webextension-polyfill';
// Now works in all browsers with Promises
const tabs = await browser.tabs.query({ active: true });
API Compatibility Matrix
Core APIs
| API | Chrome | Firefox | Safari | Edge | Notes |
|---|---|---|---|---|---|
action.* | ✓ | ✓ | ✓ | ✓ | MV3 only |
alarms.* | ✓ | ✓ | ✓ | ✓ | Standard |
bookmarks.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
browserAction.* | MV2 | ✓ | MV2 | MV2 | Use action in MV3 |
commands.* | ✓ | ✓ | ◐ | ✓ | Safari: limited |
contextMenus.* | ✓ | ✓ | ✓ | ✓ | Standard |
cookies.* | ✓ | ✓ | ◐ | ✓ | Safari: restrictions |
downloads.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
history.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
i18n.* | ✓ | ✓ | ✓ | ✓ | Standard |
identity.* | ✓ | ◐ | ✗ | ✓ | Firefox: partial |
idle.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
management.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
notifications.* | ✓ | ✓ | ✗ | ✓ | Safari: no support |
permissions.* | ✓ | ✓ | ◐ | ✓ | Safari: limited |
runtime.* | ✓ | ✓ | ✓ | ✓ | Standard |
scripting.* | ✓ | ✓ | ◐ | ✓ | Safari: limited |
storage.* | ✓ | ✓ | ✓ | ✓ | Standard |
tabs.* | ✓ | ✓ | ◐ | ✓ | Safari: some limits |
webNavigation.* | ✓ | ✓ | ◐ | ✓ | Safari: limited |
webRequest.* | ✓ | ✓ | ◐ | ✓ | Safari: observe only |
windows.* | ✓ | ✓ | ◐ | ✓ | Safari: limited |
Advanced APIs
| API | Chrome | Firefox | Safari | Edge | Workaround |
|---|---|---|---|---|---|
declarativeNetRequest | ✓ | ◐ | ◐ | ✓ | Use webRequest |
offscreen | 109+ | ✗ | ✗ | 109+ | Content script |
sidePanel | 114+ | ✗ | ✗ | 114+ | Use popup |
storage.session | 102+ | 115+ | 16.4+ | 102+ | Use local + clear |
userScripts | 120+ | ✓ | ✗ | 120+ | Content scripts |
Polyfill Setup
Using webextension-polyfill
The Mozilla webextension-polyfill normalizes the Chrome callback-style API to Firefox's Promise-based API.
Installation
npm install webextension-polyfill
# TypeScript types
npm install -D @anthropic-ai/anthropic-sdk-types/webextension-polyfill
Usage in Background Script
// background.ts
import browser from 'webextension-polyfill';
browser.runtime.onMessage.addListener(async (message, sender) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return { tabId: tabs[0]?.id };
});
Usage in Content Script
// content.ts
import browser from 'webextension-polyfill';
const response = await browser.runtime.sendMessage({ type: 'getData' });
console.log(response);
WXT Framework (Recommended)
WXT provides built-in polyfill support:
// No import needed - browser is global
export default defineContentScript({
matches: ['*://*.example.com/*'],
main() {
// browser.* works everywhere
browser.runtime.sendMessage({ type: 'init' });
},
});
Feature Detection Patterns
Check API Availability
// Check if API exists
function hasAPI(api: string): boolean {
const parts = api.split('.');
let obj: any = typeof browser !== 'undefined' ? browser : chrome;
for (const part of parts) {
if (obj && typeof obj[part] !== 'undefined') {
obj = obj[part];
} else {
return false;
}
}
return true;
}
// Usage
if (hasAPI('sidePanel.open')) {
browser.sidePanel.open({ windowId });
} else {
// Fallback to popup
browser.action.openPopup();
}
Runtime Browser Detection
// Detect browser at runtime
function getBrowser(): 'chrome' | 'firefox' | 'safari' | 'edge' | 'unknown' {
const ua = navigator.userAgent;
if (ua.includes('Firefox')) return 'firefox';
if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
if (ua.includes('Edg/')) return 'edge';
if (ua.includes('Chrome')) return 'chrome';
return 'unknown';
}
// Detect from extension APIs
function getBrowserFromAPIs(): 'chrome' | 'firefox' | 'safari' | 'edge' {
if (typeof browser !== 'undefined') {
// @anthropic-ai/anthropic-sdk-ts-expect-error - browser_specific_settings only in Firefox
if (browser.runtime.getBrowserInfo) return 'firefox';
return 'safari';
}
if (navigator.userAgent.includes('Edg/')) return 'edge';
return 'chrome';
}
Feature Flags Pattern
// features.ts
export const FEATURES = {
sidePanel: hasAPI('sidePanel'),
offscreen: hasAPI('offscreen'),
sessionStorage: hasAPI('storage.session'),
userScripts: hasAPI('userScripts'),
declarativeNetRequest: hasAPI('declarativeNetRequest'),
} as const;
// Usage
import { FEATURES } from './features';
if (FEATURES.sidePanel) {
// Use side panel
} else {
// Use popup alternative
}
Browser-Specific Patterns
Firefox-Specific
Gecko ID (Required)
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"strict_min_version": "109.0"
}
}
}
Data Collection Permissions (2025+)
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"data_collection_permissions": {
"required": [],
"optional": ["technicalAndInteraction"]
}
}
}
}
Firefox Android Support
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com"
},
"gecko_android": {
"strict_min_version": "120.0"
}
}
}
Safari-Specific
Privacy Manifest Requirement
Safari extensions require a host app with PrivacyInfo.xcprivacy:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
</dict>
</plist>
Safari Limitations Handling
// Safari doesn't support webRequest blocking
async function blockRequest(details: WebRequestDetails) {
const browser = getBrowser();
if (browser === 'safari') {
// Use declarativeNetRequest instead
await browser.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1,
action: { type: 'block' },
condition: { urlFilter: details.url }
}]
});
} else {
// Use webRequestBlocking
return { cancel: true };
}
}
Chrome-Specific
Service Worker State Persistence
// Chrome service workers terminate after ~5 minutes
// Always persist state to storage
// BAD: State lost on worker termination
let count = 0;
// GOOD: Persist to storage
const countStorage = storage.defineItem<number>('local:count', {
defaultValue: 0
});
async function increment() {
const count = await countStorage.getValue();
await countStorage.setValue(count + 1);
}
Offscreen Documents (Chrome/Edge only)
// For DOM access in MV3 service worker
if (hasAPI('offscreen')) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
}
Manifest Differences
Cross-Browser Manifest Generation
// wxt.config.ts
export default defineConfig({
manifest: ({ browser }) => ({
name: 'My Extension',
version: '1.0.0',
// Chrome/Edge
...(browser === 'chrome' && {
minimum_chrome_version: '116',
}),
// Firefox
...(browser === 'firefox' && {
browser_specific_settings: {
gecko: {
id: 'my-extension@example.com',
strict_min_version: '109.0',
},
},
}),
// Different permissions per browser
permissions: [
'storage',
'activeTab',
...(browser !== 'safari' ? ['notifications'] : []),
],
}),
});
MV2 vs MV3 Differences
| Feature | MV2 | MV3 |
|---|---|---|
| Background | backgroundcli | background.service_worker |
| Remote code | Allowed | Forbidden |
executeScript | Eval strings allowed | Functions only |
| Content security | Relaxed CSP | Strict CSP |
webRequestBlocking | Supported | Use DNR |
Testing Cross-Browser
Manual Testing Matrix
| Feature | Chrome | Firefox | Safari | Edge | Notes |
|---------|--------|---------|--------|------|-------|
| Install | [ ] | [ ] | [ ] | [ ] | |
| Popup opens | [ ] | [ ] | [ ] | [ ] | |
| Content script | [ ] | [ ] | [ ] | [ ] | |
| Background messages | [ ] | [ ] | [ ] | [ ] | |
| Storage sync | [ ] | [ ] | [ ] | [ ] | |
Automated Testing
// tests/browser-compat.test.ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';
describe('cross-browser compatibility', () => {
it('handles missing sidePanel API', async () => {
// Simulate Safari (no sidePanel)
delete (fakeBrowser as any).sidePanel;
const result = await openUI();
expect(result.method).toBe('popup');
});
it('handles missing notifications API', async () => {
delete (fakeBrowser as any).notifications;
const result = await notify('Test');
expect(result.fallback).toBe('console');
});
});
Common Compatibility Issues
Issue: tabs.query Returns Different Results
Problem: Safari returns fewer tab properties.
Solution:
const tabs = await browser.tabs.query({ active: true });
const tab = tabs[0];
// Always check property existence
const url = tab?.url ?? 'unknown';
const favIconUrl = tab?.favIconUrl ?? '/default-icon.png';
Issue: Storage Quota Differences
| Browser | Local | Sync | Session |
|---|---|---|---|
| Chrome | 10MB | 100KB | 10MB |
| Firefox | Unlimited | 100KB | 10MB |
| Safari | 10MB | 100KB | 10MB |
Solution:
async function safeStore(key: string, data: unknown) {
const size = new Blob([JSON.stringify(data)]).size;
if (size > 100 * 1024 && storageArea === 'sync') {
console.warn('Data too large for sync, using local');
await browser.storage.local.set({ [key]: data });
} else {
await browser.storage[storageArea].set({ [key]: data });
}
}
Issue: webRequest Blocking Not Working
Problem: Safari doesn't support blocking webRequests.
Solution: Use declarativeNetRequest for all browsers:
// Works in all browsers
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1],
addRules: [{
id: 1,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: '*://ads.example.com/*',
resourceTypes: ['script', 'image']
}
}]
});
References
Similar Skills
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.