Chrome Extension Manifest V3 Reference

Complete manifest.json reference with copy-ready snippets, permissions, and common patterns.

Minimal Skeleton required

Smallest valid Manifest V3 manifest.json.

json
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A short description of what the extension does."
}

Full starter template

json
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A Chrome extension built with Manifest V3.",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png"
    },
    "default_title": "My Extension"
  },
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["storage", "activeTab"]
}

Top-Level Fields

FieldTypeRequiredDescription
manifest_versionintegerYesMust be 3 for Manifest V3
namestringYesExtension name (max 45 chars)
versionstringYes1–4 dot-separated integers (0–65535), e.g. "1.0.0"
descriptionstringNoDescription (max 132 chars for CWS)
iconsobjectNoMap of size β†’ path. Required: 128Γ—128 for CWS
actionobjectNoConfigures the toolbar icon and popup
backgroundobjectNoService worker configuration
content_scriptsarrayNoScripts injected into pages
permissionsarrayNoGranted at install
optional_permissionsarrayNoRequested at runtime
host_permissionsarrayNoHost match patterns for cross-origin access
optional_host_permissionsarrayNoHost permissions requested at runtime
options_pagestringNoFull options page path
options_uiobjectNoEmbedded options page config
commandsobjectNoKeyboard shortcuts
omniboxobjectNoAddress bar keyword
web_accessible_resourcesarrayNoResources accessible from pages
content_security_policyobjectNoCSP for extension pages
minimum_chrome_versionstringNoMinimum Chrome version required
short_namestringNoFallback for limited space (max 12 chars)
homepage_urlstringNoExtension homepage
authorstringNoDeveloper email
offline_enabledbooleanNoWorks offline (default false)
incognitostringNo"spanning" | "split" | "not_allowed"
default_localestringNoe.g. "en" β€” required if _locales exists
update_urlstringNoManual update URL (non-CWS)

Version format

1–4 segments, each 0–65535. Leading zeros not allowed.

"version": "1.0.0"        // valid
"version": "2.10.3.1"     // valid (4 segments)
"version": "1.01"         // INVALID (leading zero)

Icons

"icons": {
  "16": "icons/icon16.png",
  "32": "icons/icon32.png",
  "48": "icons/icon48.png",
  "128": "icons/icon128.png"
}
Always provide 128Γ—128 β€” required by Chrome Web Store. Use PNG format.

Permissions

Permissions Table

PermissionAPI / UseWarn
activeTabTemporary access to current tab when invokedNo
storagechrome.storage.local / sync / sessionNo
tabschrome.tabs β€” query, create, update tabsYes
scriptingchrome.scripting.executeScript / insertCSSYes
alarmschrome.alarms β€” periodic background tasksNo
notificationschrome.notifications β€” system notificationsYes
contextMenuschrome.contextMenus β€” right-click menusNo
bookmarkschrome.bookmarksYes
historychrome.historyYes
cookieschrome.cookiesYes
webRequestObserve network requests (not block in MV3)Yes
declarativeNetRequestBlock/modify requests via rules (MV3 replacement for webRequestBlocking)Yes
sidePanelchrome.sidePanel β€” side panel UINo
offscreenchrome.offscreen β€” offscreen documents for DOM/audioNo
identitychrome.identity β€” OAuth2 flowYes
idlechrome.idle β€” detect user idle stateNo
powerchrome.power β€” keep awakeNo
printerProviderchrome.printerProviderNo
faviconchrome favicon APINo
topSiteschrome.topSitesYes
managementchrome.management β€” manage other extensionsYes
downloadschrome.downloadsYes
system.cpuchrome.system.cpuNo
system.memorychrome.system.memoryNo
system.storagechrome.system.storageNo
ttschrome.tts β€” text-to-speechNo
clipboardReadRead from clipboardYes
clipboardWriteWrite to clipboardNo
geolocationAccess geolocation (requires host permission)Yes
unlimitedStorageRemove 5MB storage quotaNo

Permissions snippet

json
{
  "permissions": [
    "storage",
    "activeTab",
    "scripting",
    "alarms",
    "contextMenus"
  ],
  "optional_permissions": [
    "bookmarks",
    "notifications"
  ],
  "host_permissions": [
    "https://api.example.com/*"
  ],
  "optional_host_permissions": [
    "https://*.example.com/*"
  ]
}

activeTab vs host_permissions

Use activeTab when you only need access to the current tab in response to a user click. It avoids the permission warning. Use host_permissions only when you need persistent access to specific sites.

Runtime permission request

js
// Request optional permission at runtime
chrome.permissions.request({
  permissions: ["bookmarks"],
  origins: ["https://api.example.com/*"]
}, (granted) => {
  if (granted) {
    console.log("Permission granted");
  }
});

// Check if permission is already granted
chrome.permissions.contains({
  permissions: ["bookmarks"]
}, (has) => {
  console.log("Has bookmarks:", has);
});

// Remove optional permission
chrome.permissions.remove({
  permissions: ["bookmarks"]
}, (removed) => {
  console.log("Removed:", removed);
});

Service Worker (Background)

Manifest V3 replaces persistent background pages with service workers β€” event-driven, no DOM access, can be terminated by Chrome.

Configuration

json
"background": {
  "service_worker": "background.js",
  "type": "module"
}
Add "type": "module" to use ES module imports in your service worker.

Service worker lifecycle

  • Starts on events (install, message, alarm, action click)
  • Chrome may terminate it after ~30 seconds of inactivity
  • No DOM β€” use chrome.offscreen if you need DOM/WebSocket
  • Persist data in chrome.storage, not in-memory variables
  • Use chrome.alarms instead of setInterval

Essential service worker patterns

js
// background.js β€” Service Worker

// Install event
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === "install") {
    // First install β€” set defaults
    chrome.storage.local.set({ enabled: true, count: 0 });
  } else if (details.reason === "update") {
    // Extension updated
    console.log("Updated from", details.previousVersion);
  }
});

// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "GET_DATA") {
    chrome.storage.local.get("count", (data) => {
      sendResponse({ count: data.count ?? 0 });
    });
    return true; // Keep message channel open for async response
  }
});

// Alarm for periodic tasks (replaces setInterval)
chrome.alarms.create("daily-check", { periodInMinutes: 60 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === "daily-check") {
    // Do periodic work
  }
});

// Context menu
chrome.contextMenus.create({
  id: "my-menu",
  title: "My Extension Action",
  contexts: ["selection"]
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "my-menu") {
    chrome.tabs.sendMessage(tab.id, {
      type: "SELECTION",
      text: info.selectionText
    });
  }
});

// Badge updates
chrome.action.setBadgeText({ text: "3" });
chrome.action.setBadgeBackgroundColor({ color: "#22c55e" });

V2 background page β†’ V3 service worker

V2 (Background Page)V3 (Service Worker)
background.page: "bg.html"background.service_worker: "bg.js"
Persistent (always running)Event-driven (can be terminated)
Full DOM accessNo DOM β€” use offscreen documents
setInterval / setTimeoutchrome.alarms
XMLHttpRequestfetch()
window.localStoragechrome.storage.local
chrome.browserActionchrome.action

Content Scripts

Scripts that run in the context of web pages. Can access the DOM but have limited access to Chrome APIs.

Manifest declaration

json
"content_scripts": [
  {
    "matches": ["https://*.example.com/*"],
    "js": ["content.js"],
    "css": ["styles.css"],
    "run_at": "document_idle",
    "all_frames": false,
    "match_about_blank": false,
    "world": "ISOLATED"
  }
]

run_at values

ValueWhenUse Case
document_idleAfter DOMContentLoaded + idle (default)Most cases β€” DOM is ready
document_startBefore any DOM/CSSInject CSS before paint, intercept early
document_endAfter DOM, before subresourcesAccess DOM before images/iframes load

world: "MAIN" vs "ISOLATED"

json
"content_scripts": [
  {
    "matches": ["https://*.example.com/*"],
    "js": ["content.js"],
    "world": "ISOLATED"   // Default β€” separate JS context, can use chrome.* APIs
  },
  {
    "matches": ["https://*.example.com/*"],
    "js": ["main-world.js"],
    "world": "MAIN"        // Shares page's JS context, NO chrome.* APIs
  }
]
Use "world": "MAIN" to access page's JavaScript objects (e.g., window.someFramework). Communicate back to the extension via window.postMessage.

Dynamic injection (scripting API)

js
// Requires "scripting" permission + activeTab or host_permissions
chrome.scripting.executeScript({
  target: { tabId: tabId },
  files: ["content.js"]
});

// Inline function injection
chrome.scripting.executeScript({
  target: { tabId: tabId },
  func: (color) => {
    document.body.style.backgroundColor = color;
  },
  args: ["#1a1a2e"]
});

// Insert CSS
chrome.scripting.insertCSS({
  target: { tabId: tabId },
  css: "body { border: 3px solid red; }"
});

// Or from file
chrome.scripting.insertCSS({
  target: { tabId: tabId },
  files: ["override.css"]
});

Content script ↔ Service worker messaging

js
// content.js β€” send message to service worker
chrome.runtime.sendMessage({ type: "PAGE_DATA", url: location.href });

// Listen for messages from service worker
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "HIGHLIGHT") {
    document.querySelectorAll(msg.selector).forEach(el => {
      el.style.outline = "2px solid #22c55e";
    });
  }
});

// background.js β€” send to specific tab
chrome.tabs.sendMessage(tabId, { type: "HIGHLIGHT", selector: "h1" });

Action (Browser Action)

Configures the extension's toolbar icon and popup. Replaces Manifest V2's browser_action and page_action.

Basic configuration

json
"action": {
  "default_popup": "popup.html",
  "default_icon": {
    "16": "icons/icon16.png",
    "32": "icons/icon32.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "default_title": "Click to open"
}

Icon as string shorthand

json
"action": {
  "default_popup": "popup.html",
  "default_icon": "icons/icon.png",
  "default_title": "My Extension"
}

API methods

js
// Badge
chrome.action.setBadgeText({ text: "5" });
chrome.action.setBadgeBackgroundColor({ color: "#22c55e" });
chrome.action.setBadgeTextColor({ color: "#ffffff" }); // Chrome 110+

// Title
chrome.action.setTitle({ title: "3 new items" });

// Icon (can use ImageData for dynamic icons)
chrome.action.setIcon({ path: "icons/icon-active.png" });

// Enable/disable
chrome.action.enable(tabId);
chrome.action.disable(tabId);

// Open popup programmatically (user gesture required)
chrome.action.openPopup();

// Listen for clicks (only fires if no popup defined)
chrome.action.onClicked.addListener((tab) => {
  // No default_popup set β†’ handle click here
  chrome.tabs.sendMessage(tab.id, { type: "TOGGLE" });
});
If default_popup is set, chrome.action.onClicked will NOT fire. Remove default_popup if you need to handle clicks programmatically.

Options Page

Full page

json
"options_page": "options.html"

Embedded in chrome://extensions

json
"options_ui": {
  "page": "options.html",
  "open_in_tab": true
}
Set open_in_tab: true for a full-width options page. false embeds it inside the extensions page dialog.

Commands & Shortcuts

Manifest

json
"commands": {
  "_execute_action": {
    "suggested_key": {
      "default": "Ctrl+Shift+Y",
      "mac": "Command+Shift+Y"
    },
    "description": "Open extension popup"
  },
  "custom-command": {
    "suggested_key": {
      "default": "Alt+Shift+G"
    },
    "description": "Run custom action"
  }
}

Handle commands in service worker

js
chrome.commands.onCommand.addListener((command) => {
  if (command === "custom-command") {
    console.log("Custom command triggered");
  }
  // "_execute_action" is handled automatically β€” opens popup
});

Omnibox

Configuration

json
"omnibox": {
  "keyword": "myext"
}

Handle omnibox input

js
// User types "myext search query" in address bar
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
  suggest([
    { content: text + " option1", description: "First suggestion" },
    { content: text + " option2", description: "Second suggestion" }
  ]);
});

chrome.omnibox.onInputEntered.addListener((text, disposition) => {
  // disposition: "currentTab" | "newForegroundTab" | "newBackgroundTab"
  chrome.tabs.update({ url: "https://example.com/search?q=" + encodeURIComponent(text) });
});

Declarative Net Request

Manifest V3 replacement for webRequestBlocking. Define rules to block, redirect, or modify requests.

Manifest

json
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
  "rule_resources": [
    {
      "id": "ruleset_1",
      "enabled": true,
      "path": "rules/rules.json"
    }
  ]
},
"host_permissions": [
  "*://*.example.com/*"
]

Static rules file (rules/rules.json)

json
[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "block"
    },
    "condition": {
      "urlFilter": "||ads.example.com^",
      "resourceTypes": ["script", "image", "xmlhttprequest"]
    }
  },
  {
    "id": 2,
    "priority": 2,
    "action": {
      "type": "redirect",
      "redirect": {
        "url": "https://cdn.example.com/clean.js"
      }
    },
    "condition": {
      "urlFilter": "||tracker.example.com/analytics.js",
      "resourceTypes": ["script"]
    }
  },
  {
    "id": 3,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        { "header": "X-Custom-Header", "operation": "set", "value": "my-value" }
      ],
      "responseHeaders": [
        { "header": "x-frame-options", "operation": "remove" }
      ]
    },
    "condition": {
      "urlFilter": "*://example.com/*",
      "resourceTypes": ["main_frame", "sub_frame"]
    }
  }
]

Dynamic rules via API

js
chrome.declarativeNetRequest.updateDynamicRules({
  addRules: [{
    id: 1001,
    priority: 1,
    action: { type: "block" },
    condition: {
      urlFilter: "||blocked.example.com^",
      resourceTypes: ["script"]
    }
  }],
  removeRuleIds: [1001]  // Remove previous version
});

Web Accessible Resources

Resources in your extension that web pages can access.

json
"web_accessible_resources": [
  {
    "resources": ["images/icon.png", "styles/inject.css"],
    "matches": ["https://*.example.com/*"]
  },
  {
    "resources": ["lib/helper.js"],
    "extension_ids": ["other-extension-id"],
    "matches": [""]
  }
]
Access from page: chrome-extension://YOUR_EXTENSION_ID/images/icon.png. In content scripts, use chrome.runtime.getURL("images/icon.png") to get the full URL.

Common Patterns

Popup β†’ Service Worker β†’ Content Script chain

js
// popup.js
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  chrome.tabs.sendMessage(tabs[0].id, { type: "ACTION", payload: "go" });
});

// background.js (relay if needed)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "RELAY") {
    chrome.tabs.sendMessage(msg.tabId, msg.payload, sendResponse);
    return true;
  }
});

// content.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "ACTION") {
    // Do something on the page
    sendResponse({ success: true });
  }
});

Content script β†’ Service Worker with response

js
// content.js
chrome.runtime.sendMessage(
  { type: "FETCH_DATA", url: "https://api.example.com/data" },
  (response) => {
    console.log("Got:", response.data);
  }
);

// background.js (has host_permissions for the API)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "FETCH_DATA") {
    fetch(msg.url)
      .then(r => r.json())
      .then(data => sendResponse({ data }))
      .catch(err => sendResponse({ error: err.message }));
    return true; // Required for async sendResponse
  }
});

Side panel extension

json
{
  "manifest_version": 3,
  "name": "Side Panel Extension",
  "version": "1.0.0",
  "permissions": ["sidePanel", "activeTab", "storage"],
  "side_panel": {
    "default_path": "sidepanel.html"
  },
  "action": {
    "default_title": "Open Side Panel"
  },
  "background": {
    "service_worker": "background.js"
  }
}
js
// background.js β€” Open side panel on action click
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.sidePanel.open({ tabId: tab.id });
});

Storage with schema

js
// defaults.js
const DEFAULTS = {
  theme: "dark",
  enabled: true,
  recentItems: [],
  maxItems: 50
};

// Install defaults
chrome.runtime.onInstalled.addListener(() => {
  chrome.storage.local.get(Object.keys(DEFAULTS), (data) => {
    const missing = {};
    for (const [key, value] of Object.entries(DEFAULTS)) {
      if (!(key in data)) missing[key] = value;
    }
    if (Object.keys(missing).length) {
      chrome.storage.local.set(missing);
    }
  });
});

// Storage change listener
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === "local" && changes.theme) {
    console.log("Theme changed to:", changes.theme.newValue);
  }
});

// Sync storage (synced across devices, 100KB limit)
chrome.storage.sync.set({ theme: "dark" });
chrome.storage.sync.get("theme", (data) => console.log(data.theme));

// Session storage (cleared on browser close)
chrome.storage.session.set({ tempData: "value" });

Alarms β€” periodic background tasks

js
// Create alarm β€” minimum period is 1 minute in MV3
chrome.alarms.create("sync", { periodInMinutes: 5 });

// One-time alarm
chrome.alarms.create("delayed-action", { delayInMinutes: 2 });

// Listen
chrome.alarms.onAlarm.addListener((alarm) => {
  switch (alarm.name) {
    case "sync":
      // fetch latest data
      break;
    case "delayed-action":
      // one-time task
      chrome.alarms.clear("delayed-action");
      break;
  }
});

Internationalization (_locales)

json
// _locales/en/messages.json
{
  "extName": {
    "message": "My Extension",
    "description": "Extension name"
  },
  "popupTitle": {
    "message": "Click to start",
    "description": "Popup title text"
  },
  "itemCount": {
    "message": "$COUNT$ items found",
    "description": "Shows item count",
    "placeholders": {
      "count": {
        "content": "$1",
        "example": "5"
      }
    }
  }
}
json
// manifest.json with i18n
{
  "name": "__MSG_extName__",
  "description": "__MSG_extDesc__",
  "default_locale": "en"
}
js
// In JS
chrome.i18n.getMessage("extName");
chrome.i18n.getMessage("itemCount", "5");

V2 β†’ V3 Migration Checklist

V2V3Action
"manifest_version": 2"manifest_version": 3Change version
background.scripts or background.pagebackground.service_workerMigrate to service worker
browser_action / page_actionactionReplace key name
chrome.browserActionchrome.actionUpdate API calls
permissions includes hostshost_permissionsMove host patterns
webRequestBlockingdeclarativeNetRequestUse declarative rules
content_security_policy (string)content_security_policy.extension_pagesChange to object
Code in <script> tags in HTMLSeparate JS filesMove inline code to files
chrome.extension.getURLchrome.runtime.getURLUpdate method
chrome.extension.sendRequestchrome.runtime.sendMessageUpdate method
Persistent backgroundEvent-driven service workerReplace setInterval with alarms
DOMParser in backgroundOffscreen documentUse chrome.offscreen

CSP format change

json
// V2 (string)
"content_security_policy": "script-src 'self'; object-src 'self'"

// V3 (object)
"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'",
  "sandbox": "sandbox allow-scripts allow-forms; script-src 'self'"
}

web_accessible_resources format change

json
// V2 (flat array)
"web_accessible_resources": ["images/icon.png", "lib/inject.js"]

// V3 (array of objects)
"web_accessible_resources": [
  {
    "resources": ["images/icon.png", "lib/inject.js"],
    "matches": ["https://*.example.com/*"]
  }
]