Manifest V3 Reference
Complete reference for Manifest V3 browser extension development with cross-browser compatibility notes, Firefox MV2 fallbacks, and Safari-specific considerations.
Manifest Version Comparison
| Feature | MV2 | MV3 | Notes |
|---|
| Background | Persistent page | Service worker | No DOM access in MV3 |
| Remote code | Allowed | Forbidden | Must bundle all scripts |
| Host permissions | In permissions | Separate field | More granular control |
| Content scripts | Same | Same | No changes |
| Web accessible | Array | Object with matches | Per-resource rules |
| CSP | Configurable | Restricted | No unsafe-eval |
| Action | browserAction/pageAction | action | Unified API |
| Declarative | Optional | Required for webRequest | declarativeNetRequest |
Browser Support Matrix
| Browser | MV3 Status | MV2 Status | Minimum Version |
|---|
| Chrome | Required | Deprecated | 88+ (full), 102+ (service workers) |
| Firefox | Supported | Supported | 109+ (MV3), 48+ (MV2) |
| Safari | Required | Not supported | 15.4+ |
| Edge | Required | Deprecated | 88+ |
Core Manifest Structure
Minimal MV3 Manifest
{
"manifest_version": 3,
"name": "Extension Name",
"version": "1.0.0",
"description": "Brief description (max 132 chars for Chrome)",
"icons": {
"16": "icons/16.png",
"32": "icons/32.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"action": {
"default_icon": "icons/48.png",
"default_popup": "popup.html",
"default_title": "Click to open"
},
"permissions": [
"storage"
],
"background": {
"service_worker": "background.js",
"type": "module"
}
}
Firefox-Specific Fields
{
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "109.0",
"strict_max_version": "130.*",
"data_collection_permissions": {
"required": [],
"optional": ["technicalAndInteraction"]
}
}
}
}
Safari-Specific Considerations
Safari extensions require:
- Xcode project wrapper
- App Store distribution
- Privacy manifest (
PrivacyInfo.xcprivacy)
- Code signing
# Convert existing extension
xcrun safari-web-extension-converter ./extension-dir \
--project-location ./safari-project \
--app-name "My Extension"
Background Scripts
MV3 Service Worker
// background.ts (MV3)
// Service worker - no DOM, no persistent state
// Use alarms for periodic tasks
chrome.alarms.create('periodic-task', { periodInMinutes: 5 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'periodic-task') {
performTask();
}
});
// State must be stored explicitly
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.set({ initialized: true });
});
// Handle messages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message).then(sendResponse);
return true; // Async response
});
MV2 Background Page (Firefox fallback)
{
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
// background.js (MV2)
// Event page with DOM access
let state = {};
browser.runtime.onMessage.addListener((message, sender) => {
return handleMessage(message);
});
// Can use DOM APIs
const parser = new DOMParser();
Cross-Browser Background Detection
// Detect environment
const isServiceWorker = typeof ServiceWorkerGlobalScope !== 'undefined'
&& self instanceof ServiceWorkerGlobalScope;
const isEventPage = typeof window !== 'undefined';
// Choose appropriate APIs
function createOffscreenDocument() {
if (chrome.offscreen) {
return chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
}
// MV2/Firefox: Use background page DOM directly
return Promise.resolve();
}
Permissions
Permission Types
| Type | MV3 Location | Purpose |
|---|
| API permissions | permissions | Access to browser APIs |
| Host permissions | host_permissions | Access to web content |
| Optional | optional_permissions | Runtime-requested |
| Optional host | optional_host_permissions | Runtime-requested URLs |
Permission Reference
| Permission | Description | User Warning |
|---|
activeTab | Current tab on user action | None |
alarms | Schedule code execution | None |
bookmarks | Read/write bookmarks | Yes |
clipboardRead | Read clipboard | Yes |
clipboardWrite | Write clipboard | None |
contextMenus | Custom context menus | None |
cookies | Read/write cookies | Yes |
declarativeNetRequest | Modify network requests | None |
downloads | Manage downloads | None |
geolocation | Access location | Yes (at use) |
history | Browser history | Yes |
identity | OAuth flows | None |
notifications | System notifications | None |
scripting | Execute scripts | None |
storage | Extension storage | None |
tabs | Tab URLs and titles | Yes |
webNavigation | Navigation events | None |
webRequest | Observe requests | None (MV3) |
Host Permissions
{
"host_permissions": [
"https://api.example.com/*",
"*://*.example.org/*"
],
"optional_host_permissions": [
"<all_urls>"
]
}
Permission Patterns
| Pattern | Matches | Notes |
|---|
<all_urls> | All URLs | Requires justification |
*://*/* | All HTTP(S) | Same as all_urls for web |
https://*.example.com/* | Subdomains | Single domain family |
https://example.com/api/* | Path prefix | Most restrictive |
Cross-Browser Permission Differences
| Feature | Chrome | Firefox | Safari |
|---|
activeTab + scripting | Full injection | Full injection | Limited to declared |
declarativeNetRequest | Full support | Partial | Full support |
offscreen | Supported | Not supported | Not supported |
sidePanel | Supported | Not supported | Not supported |
Action API
MV3 Unified Action
// MV3: Single unified action API
chrome.action.setIcon({ path: 'icons/active.png' });
chrome.action.setBadgeText({ text: '5' });
chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
chrome.action.setTitle({ title: 'New title' });
chrome.action.onClicked.addListener((tab) => {
// No popup defined - handle click
});
MV2 browserAction/pageAction
// MV2: Separate APIs
browser.browserAction.setIcon({ path: 'icons/active.png' });
browser.pageAction.show(tabId);
Cross-Browser Action Pattern
// Unified wrapper
const action = chrome.action || chrome.browserAction || browser.browserAction;
function setIcon(path: string) {
return action.setIcon({ path });
}
function setBadge(text: string) {
return action.setBadgeText({ text });
}
Content Scripts
Manifest Declaration
{
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_idle",
"all_frames": false,
"match_about_blank": false,
"world": "ISOLATED"
}
]
}
Programmatic Injection (MV3)
// MV3: scripting API
chrome.scripting.executeScript({
target: { tabId },
files: ['inject.js'],
world: 'ISOLATED'
});
// With function
chrome.scripting.executeScript({
target: { tabId },
func: (arg) => {
console.log('Injected with', arg);
},
args: ['argument']
});
Programmatic Injection (MV2)
// MV2: tabs API
browser.tabs.executeScript(tabId, {
file: 'inject.js',
runAt: 'document_idle'
});
World Isolation
| World | Access | Use Case |
|---|
ISOLATED (default) | Own JS context | Most extensions |
MAIN | Page's JS context | Page script modification |
// MAIN world injection (MV3)
chrome.scripting.executeScript({
target: { tabId },
func: () => {
// Can access page's window, modify prototypes
window.pageVariable = 'modified';
},
world: 'MAIN'
});
Network Request Handling
Declarative Net Request (MV3)
{
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}
]
}
}
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||ads.example.com",
"resourceTypes": ["script", "image"]
}
},
{
"id": 2,
"priority": 2,
"action": {
"type": "redirect",
"redirect": { "url": "https://example.com/blocked" }
},
"condition": {
"urlFilter": "tracker.js",
"resourceTypes": ["script"]
}
}
]
Dynamic Rules (MV3)
// Add rules at runtime
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [
{
id: 1000,
priority: 1,
action: { type: 'block' },
condition: { urlFilter: userBlockedDomain }
}
]
});
WebRequest (MV2)
// MV2: Blocking webRequest
browser.webRequest.onBeforeRequest.addListener(
(details) => {
if (shouldBlock(details.url)) {
return { cancel: true };
}
},
{ urls: ['<all_urls>'] },
['blocking']
);
Cross-Browser Network Handling
// Check for MV3 declarativeNetRequest
if (chrome.declarativeNetRequest) {
// Use declarative rules
setupDeclarativeRules();
} else if (browser.webRequest) {
// Fall back to MV2 webRequest
setupWebRequestListeners();
}
Web Accessible Resources
MV3 Syntax
{
"web_accessible_resources": [
{
"resources": ["inject.js", "styles.css"],
"matches": ["https://*.example.com/*"],
"use_dynamic_url": true
},
{
"resources": ["public/*"],
"matches": ["<all_urls>"],
"extension_ids": []
}
]
}
MV2 Syntax
{
"web_accessible_resources": [
"inject.js",
"styles.css",
"public/*"
]
}
Accessing Resources
// Get resource URL
const url = chrome.runtime.getURL('inject.js');
// chrome-extension://EXTENSION_ID/inject.js
// With dynamic URL (MV3)
// chrome-extension://EXTENSION_ID/RANDOM_TOKEN/inject.js
Messaging
Internal Messaging
// Send from content script to background
chrome.runtime.sendMessage({ type: 'getData' }, (response) => {
console.log('Received:', response);
});
// Background listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getData') {
getData().then(sendResponse);
return true; // Async
}
});
Tab Messaging
// Background to specific tab
chrome.tabs.sendMessage(tabId, { type: 'update' });
// Content script listener
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'update') {
updateUI();
}
});
External Messaging
{
"externally_connectable": {
"matches": ["https://app.example.com/*"]
}
}
// From web page
chrome.runtime.sendMessage(extensionId, { type: 'request' }, (response) => {
console.log('Extension responded:', response);
});
// In extension
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
// Validate sender.url
if (isAllowedOrigin(sender.url)) {
handleExternalMessage(message).then(sendResponse);
return true;
}
});
Storage API
Storage Types
| Type | Quota | Sync | Persistence |
|---|
local | 10MB (unlimited with permission) | No | Until cleared |
sync | 100KB total, 8KB/item | Yes | Cross-device |
session | 10MB | No | Until browser close |
managed | N/A | N/A | Admin-configured |
Usage
// Set values
await chrome.storage.local.set({ key: 'value' });
// Get values
const result = await chrome.storage.local.get(['key']);
console.log(result.key);
// Remove values
await chrome.storage.local.remove(['key']);
// Listen for changes
chrome.storage.onChanged.addListener((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key} changed from ${oldValue} to ${newValue}`);
}
});
Session Storage (MV3)
// Memory-only storage - cleared when browser closes
await chrome.storage.session.set({ temporaryData: value });
// Set access level for content scripts
await chrome.storage.session.setAccessLevel({
accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS'
});
Alarms API
// Create alarm
chrome.alarms.create('myAlarm', {
delayInMinutes: 1,
periodInMinutes: 5
});
// Listen for alarm
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'myAlarm') {
performPeriodicTask();
}
});
// Clear alarm
chrome.alarms.clear('myAlarm');
Offscreen Documents (MV3 Chrome only)
// Create offscreen document for DOM access
async function createOffscreen() {
if (await chrome.offscreen.hasDocument()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER', 'CLIPBOARD'],
justification: 'Parse HTML and access clipboard'
});
}
// Communicate with offscreen document
chrome.runtime.sendMessage({ target: 'offscreen', data: htmlContent });
Reasons: AUDIO_PLAYBACK, BLOBS, CLIPBOARD, DOM_PARSER, DOM_SCRAPING, GEOLOCATION, LOCAL_STORAGE, MATCH_MEDIA, TESTING, USER_MEDIA, WEB_RTC, WORKERS
Side Panel (MV3 Chrome only)
{
"side_panel": {
"default_path": "sidepanel.html"
},
"permissions": ["sidePanel"]
}
// Open side panel
chrome.sidePanel.open({ tabId });
// Set panel behavior
chrome.sidePanel.setOptions({
tabId,
path: 'sidepanel.html',
enabled: true
});
Cross-Browser Compatibility Patterns
API Detection
// Check for API availability
function hasAPI(name: string): boolean {
const parts = name.split('.');
let obj: any = chrome;
for (const part of parts) {
if (obj[part] === undefined) return false;
obj = obj[part];
}
return true;
}
// Usage
if (hasAPI('offscreen.createDocument')) {
// Chrome MV3 offscreen
} else if (hasAPI('tabs.executeScript')) {
// MV2 injection
}
Browser-Specific Manifest
// manifest.json for Chrome
{
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
}
// manifest.json for Firefox
{
"manifest_version": 3,
"background": {
"scripts": ["background.js"]
},
"browser_specific_settings": {
"gecko": { "id": "extension@example.com" }
}
}
WXT Cross-Browser Handling
// wxt.config.ts
export default defineConfig({
manifest: {
// Common fields
name: 'Extension',
// Browser-specific overrides
$browser_specific: {
firefox: {
browser_specific_settings: {
gecko: { id: 'extension@example.com' }
}
}
}
}
});
Migration Checklist: MV2 to MV3
Required Changes
Code Changes
Testing
Related Resources
- wxt-framework-patterns skill: WXT-specific patterns
- cross-browser-compatibility skill: API compatibility matrices
- extension-security skill: Security best practices