Sebastian Gomez
How to Use Context Menus in Chrome Extensions
In this post, we will explore how we can add functionality to context menus (the menus that appear on right click) in Chrome using the available APIs. This guide is useful for anyone building extensions who wants to improve how they interact with their users.
Remember that this is a series of posts about Chrome extensions. If you are coming from the beginning, in chapter 2 we set up manifest.json with Manifest V3 and the background service worker, concepts we will use here.
What Are Context Menus?
Context menus in Chrome are the ones that appear when you right click on a web page. Chrome lets extension developers add custom options to these menus, offering a better user experience.
Initial Setup
To start working with context menus, we first need to grant the required permissions in our extension's manifest.json file:
{
"permissions": ["contextMenus", "scripting"],
"host_permissions": ["<all_urls>"]
}We also add the scripting permission and host_permissions because later on we will show the result directly on the page with chrome.scripting.executeScript.
A note on the lifecycle. In Manifest V3 the background script (background.js) is a non persistent service worker: Chrome stops it and recreates it as needed. That is why menu creation must not happen at the top level of the file, since it could be lost when the worker restarts. The correct approach is to create the menus inside the chrome.runtime.onInstalled event, which runs once when the extension is installed or updated.
Creating a Context Menu
Let's create a basic context menu that will appear anywhere on the web page. As we explained above, we register all menu creation inside chrome.runtime.onInstalled in our background.js:
// background.js
chrome.runtime.onInstalled.addListener(() => {
// Main menu
chrome.contextMenus.create({
id: "myContextMenu",
title: "My Context Menu",
contexts: ["all"],
});
// Submenus: nested under the main menu using parentId
chrome.contextMenus.create({
id: "subMenu1",
parentId: "myContextMenu",
title: "Submenu 1",
contexts: ["all"],
});
chrome.contextMenus.create({
id: "subMenu2",
parentId: "myContextMenu",
title: "Submenu 2",
contexts: ["all"],
});
// Option that only appears when there is selected text
chrome.contextMenus.create({
id: "wordCount",
title: "Count Words",
contexts: ["selection"],
});
});As you can see, each menu is defined with a unique id, a title, and the list of contexts in which it should be shown. The submenus use parentId to hang off the main menu, and the wordCount option uses the selection context so it appears only when the user has selected text.
Handling Click Events
To handle actions when the user selects a menu option, we register a single chrome.contextMenus.onClicked listener and decide what to do with a switch on info.menuItemId. Having a single listener matters in a service worker: it avoids registering several handlers that would all run on every click, and it keeps the logic in one place.
// background.js (continued)
chrome.contextMenus.onClicked.addListener((info, tab) => {
switch (info.menuItemId) {
case "subMenu1":
console.log("Submenu 1 selected");
break;
case "subMenu2":
console.log("Submenu 2 selected");
break;
case "wordCount":
if (info.selectionText) {
const wordCount = info.selectionText.trim().split(/\s+/).length;
showWordCount(tab.id, wordCount);
}
break;
}
});Example: Counting Selected Words
Suppose we want to count the words the user has selected. We already created the wordCount option and wired it into the switch above; now we just need to show the result.
Here is an important detail. In Manifest V3 the background code runs in a service worker, which has no access to the DOM, so `alert()` is not available and would throw an error. Instead, we inject a small script into the active tab with chrome.scripting.executeScript to show the count on the page itself:
// background.js (continued)
function showWordCount(tabId, count) {
chrome.scripting.executeScript({
target: { tabId },
// The argument is passed to the injected script through "args"
args: [count],
func: (wordCount) => {
// This code does run on the page, where the DOM exists
window.alert(`Number of words: ${wordCount}`);
},
});
}Tip. If you prefer not to interrupt the user with a dialog, other correct alternatives for a service worker are sending a message to a content script with chrome.tabs.sendMessage or showing a system notification with chrome.notifications. The key point is to never use DOM APIs directly from the service worker.
Removing and Updating Context Menus
Besides creating context menus, we can also remove or update them as needed.
To remove a context menu, we use chrome.contextMenus.remove:
chrome.contextMenus.remove("subMenu2", () => {
console.log("Submenu 2 removed");
});To update a context menu, we use chrome.contextMenus.update:
chrome.contextMenus.update("myContextMenu", {
title: "My Updated Context Menu",
});You can run these operations inside your own listeners (for example, in response to another click or a message); they do not need to live inside onInstalled.
Conclusion
Context menus are a powerful tool for improving the interactivity of your Chrome extensions. With the chrome.contextMenus API you can customize these menus to offer specific functionality that improves the user experience. The key under Manifest V3 is to respect the service worker lifecycle: create the menus in onInstalled, use a single onClicked listener, and show results on the page with chrome.scripting, not with DOM APIs.
Suggested exercises
- Create an extension with a context menu that appears only over images (the
imagecontext) and that logs the image URL to the console on click. - Extend the "Count Words" example so that, besides the total, it also shows how many characters the selection has. Pass them as arguments to the injected script.
- Replace the injected
window.alertwith a system notification usingchrome.notifications. Compare the two user experiences.
3-point summary
- In Manifest V3 menus are created inside
chrome.runtime.onInstalledbecause the background service worker is not persistent. - Handle all clicks with a single
chrome.contextMenus.onClickedlistener and aswitch (info.menuItemId)to keep the logic tidy. - The service worker has no DOM, so instead of
alert()you show results on the page withchrome.scripting.executeScript, a message to a content script, orchrome.notifications.
That's all. I hope this post helped you understand how to work with context menus in Chrome and that you can apply it to an extension you have in mind.
Leave me a comment if it helped, if you want to add an opinion, or if you have any questions. And remember, if you liked it, you can also share it using the social links below. See you next time!
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.