Tutorial · 8 min read

Chrome Extension Context Menu Tutorial

How to add right-click menu entries to a Chrome extension and handle clicks from the service worker.

Illustration for the ManifestGo article: Chrome Extension Context Menu Tutorial

Context menus are one of the highest-value, lowest-effort features you can add. Highlight text, right-click, and your extension does something useful — no popup required.

Create menus at install time

Add "contextMenus" to permissions, then create items inside chrome.runtime.onInstalled. Creating them at the top level of a service worker causes duplicate-id errors when the worker restarts.

chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: "save-selection", title: "Save \"%s\" to my list", contexts: ["selection"] }); });

Useful contexts

  • selection — highlighted text; %s in the title is replaced by the selection.
  • link — right-click on an anchor; info.linkUrl holds the target.
  • image — info.srcUrl gives the image address.
  • page — anywhere on the page, good for a generic action.

Handling the click

chrome.contextMenus.onClicked.addListener((info, tab) => ...) runs in the service worker. Use info.menuItemId to branch, and chrome.scripting.executeScript or a message to the tab if you need page access.

Nested and parent menu items

Pass a parentId to chrome.contextMenus.create() to build submenus: create a parent with id "my-tools", then create children with parentId: "my-tools" to group several related actions under one top-level entry instead of cluttering the right-click menu with five separate items.

Checkbox and radio menu items

  • type: "checkbox" toggles a checked boolean each click and is read back from info.checked in onClicked.
  • type: "radio" groups items sharing the same parentId so only one is checked at a time — useful for a 3-option mode switcher.
  • Persist the chosen state to chrome.storage so the checked state survives a service worker restart, since Chrome does not remember it for you across restarts in all cases.

Updating and removing items

chrome.contextMenus.update("save-selection", { title: "Saved!" }) changes an existing item without recreating it, and chrome.contextMenus.remove(id) or removeAll() cleans up. Always guard update()/remove() calls in a try/catch or check chrome.runtime.lastError, since calling them on an id that no longer exists throws.

Reaching into the page from a menu click

chrome.contextMenus.onClicked cannot touch the DOM directly. Use chrome.scripting.executeScript({ target: { tabId: tab.id }, func: yourFunction, args: [info] }) to run code in the page, which requires the "scripting" permission and either activeTab or a matching host_permissions entry.

Menu items scoped to specific extension surfaces

The "action" context targets your own toolbar icon's right-click menu, separate from page contexts — useful for adding an "Open settings" or "Report a bug" entry directly on your extension's icon.

Frequently asked questions

Why do I get 'Cannot create item with duplicate id'?

The service worker restarted and re-ran your create call. Wrap creation in chrome.runtime.onInstalled, or call chrome.contextMenus.removeAll() first.

Can context menu titles be dynamic per page?

Yes — call chrome.contextMenus.update() from a tab-activation or navigation listener to change the title based on the current page before the user right-clicks.

Do context menus work in incognito?

Only if the user has explicitly enabled 'Allow in Incognito' for your extension on chrome://extensions; otherwise the service worker and its menu items never load in that window.

What's the contexts value for the extension's own popup?

There is no popup context for contextMenus — right-clicking inside your popup shows Chrome's default browser context menu, not one you can customize with the contextMenus API.

Keep reading