Build Custom Context Menu Chrome Extension: Complete Guide

24 min read

Build Custom Context Menu Chrome Extension: Complete Guide

Build Custom Context Menu Chrome Extension: Complete Guide

The Chrome Context Menu API is one of the most powerful features available for extension developers, allowing you to add custom options to the right-click menu that appears when users right-click on pages, links, images, or other elements in Chrome. Whether you want to create a quick action to save selected text, translate foreign language content, search selected terms in different services, or perform any number of other useful operations, understanding how to properly implement the chrome.contextMenus API is essential for building extensions that feel integrated and intuitive.

This comprehensive tutorial will walk you through everything you need to know to build professional-grade custom context menu extensions using Manifest V3. We’ll cover the complete Context Menu API, including creating static and dynamic menus, handling click events, working with different context types, and implementing best practices for user experience and performance. By the end of this guide, you’ll have the knowledge and practical examples needed to create powerful right-click extensions that enhance users’ browsing experience.


Understanding the Chrome Context Menu API

The Chrome Context Menu API, accessible through the chrome.contextMenus namespace, provides developers with the ability to add, update, and remove items from Chrome’s right-click context menu. This API has been a cornerstone of Chrome extension development for years, enabling countless popular extensions that millions of users rely on daily for enhanced productivity and functionality.

When you implement a custom context menu, you’re essentially extending Chrome’s built-in right-click functionality with your own custom actions. These actions can range from simple operations like copying formatted text to complex workflows involving multiple steps, API calls, or interactions with other parts of your extension. The API supports both static menus that are defined at installation time and dynamic menus that can be created or modified in response to user actions or page content.

One of the key concepts to understand is that context menus in Chrome are organized in a hierarchical structure. You can create parent menus with nested submenus, allowing you to organize related actions together and provide users with a clean, intuitive interface. This hierarchical structure also allows you to group similar functionality together, making your extension more organized and easier to use as your feature set grows.

When to Use Custom Context Menus

Custom context menus are particularly useful in several common scenarios that can significantly enhance user productivity. First, they’re ideal for adding quick actions that apply to selected text, such as searching the selection in a specific service, translating it, or saving it to a notes application. Second, they’re perfect for adding page-level actions that should be accessible from anywhere on a page, like sharing the current page, saving it for later, or extracting specific information. Third, context menus excel at providing targeted actions for specific element types, such as image-specific actions like reverse image search or downloading the image.

Understanding when to use context menus versus other UI patterns is important for creating a cohesive user experience. Context menus are best for actions that are context-dependent and should be available without requiring users to click through multiple pages or navigate to specific areas of your extension. They’re designed for quick, one-click actions that users can access intuitively through a familiar interface pattern.


Setting Up Your Extension Manifest

Before you can start implementing custom context menus, you need to configure your extension’s manifest.json file with the appropriate permissions and configuration. The Context Menu API requires specific permissions to function properly, and understanding these requirements is crucial for building a working extension.

Required Permissions

To use the Chrome Context Menu API, you must declare the "contextMenus" permission in your manifest.json file. This permission grants your extension the ability to create, modify, and remove context menu items. Additionally, depending on what your extension needs to do when a context menu item is clicked, you may need other permissions such as "activeTab" for accessing the current tab, "storage" for persisting data, or permissions for specific APIs like "bookmarks" or "history".

Here’s an example manifest configuration for a basic custom context menu extension:

{
  "name": "My Custom Context Menu",
  "version": "1.0",
  "manifest_version": 3,
  "description": "A custom context menu extension demonstration",
  "permissions": [
    "contextMenus",
    "activeTab"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  }
}

It’s worth noting that in Manifest V3, the approach to permissions has evolved from earlier versions. The contextMenus permission is a required permission that will be displayed to users during installation. If your extension only needs to create context menus that operate on specific pages or require minimal permissions, you can potentially use the more limited "activeTab" permission, which grants temporary access to the current tab when the user invokes your extension.

Understanding Context Types

The Context Menu API supports various context types that determine when your menu items appear. The "contexts" property in your menu item configuration allows you to specify which contexts should trigger your menu item. Common context types include "all" for appearing in all contexts, "page" for appearing when right-clicking on a page background, "selection" for appearing when text is selected, "link" for appearing when right-clicking on a hyperlink, "image" for appearing when right-clicking on an image, and "video" and "audio" for media elements.

You can also use more specific contexts like "editable" for text input fields, "launcher" for the Chrome app launcher, and "browser_action" or "page_action" for extension action icons. Understanding which contexts to use is crucial for creating a context menu that appears in the right situations without cluttering the menu with irrelevant options.


Creating Basic Context Menu Items

Now that you understand the foundational concepts, let’s dive into implementing your first custom context menu. The core of working with the Context Menu API involves using the chrome.contextMenus.create() method to add items to the menu and handling click events through the onClicked event listener.

Your First Context Menu Item

Creating a basic context menu item is straightforward. In your background service worker, you call chrome.contextMenus.create() with a configuration object that specifies the menu item’s properties. The most important properties are "id" (a unique identifier for the menu item), "title" (the text that will appear in the context menu), and "contexts" (an array of contexts where the item should appear).

Here’s a simple example that creates a context menu item that appears when text is selected:

// background.js
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "searchSelectedText",
    title: "Search with My Extension",
    contexts: ["selection"]
  });
  
  chrome.contextMenus.create({
    id: "saveToNotebook",
    title: "Save to Notebook",
    contexts: ["selection", "page"]
  });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "searchSelectedText") {
    const selectedText = info.selectionText;
    // Perform search action
    chrome.tabs.create({
      url: `https://example.com/search?q=${encodeURIComponent(selectedText)}`
    });
  } else if (info.menuItemId === "saveToNotebook") {
    // Handle save to notebook action
    console.log("Saving to notebook:", info.selectionText || info.pageUrl);
  }
});

This basic example demonstrates several key concepts. First, we create the context menu items inside the chrome.runtime.onInstalled event listener, which ensures the menu items are created when the extension is installed or updated. Second, we specify different contexts for different menu items, allowing them to appear in appropriate situations. Third, we handle click events through the onClicked listener, which receives information about what was clicked and which tab the click occurred in.

Handling Different Click Contexts

The info object passed to your click handler contains valuable information about the context of the click, including selectionText (the selected text if any), linkUrl (the URL of a link if clicked on a link), pageUrl (the URL of the page), srcUrl (the source URL for images or media), and many other properties depending on what was clicked. Your handler should check which properties are available and handle each case appropriately.

For example, if your context menu appears on both selected text and images, you need to handle both cases:

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "quickAction") {
    if (info.selectionText) {
      // Handle selected text
      processSelectedText(info.selectionText);
    } else if (info.srcUrl) {
      // Handle image
      processImage(info.srcUrl);
    } else if (info.linkUrl) {
      // Handle link
      processLink(info.linkUrl);
    } else {
      // Handle page click
      processPage(info.pageUrl);
    }
  }
});

Creating Hierarchical Menu Structures

As your extension grows, you’ll likely want to organize your context menu items into submenus for better organization and user experience. The Context Menu API supports creating nested menus through the "parentId" property, allowing you to create multi-level menu structures.

Creating Submenus

To create a submenu, you first create a parent menu item with the type “normal” (which is the default), and then create child items that reference the parent ID. Here’s an example that creates a search submenu with multiple search engine options:

chrome.runtime.onInstalled.addListener(() => {
  // Create parent menu
  chrome.contextMenus.create({
    id: "searchMenu",
    title: "Search With...",
    contexts: ["selection"]
  });
  
  // Create child items under the parent
  chrome.contextMenus.create({
    id: "searchGoogle",
    parentId: "searchMenu",
    title: "Google",
    contexts: ["selection"]
  });
  
  chrome.contextMenus.create({
    id: "searchBing",
    parentId: "searchMenu",
    title: "Bing",
    contexts: ["selection"]
  });
  
  chrome.contextMenus.create({
    id: "searchDuckDuckGo",
    parentId: "searchMenu",
    title: "DuckDuckGo",
    contexts: ["selection"]
  });
  
  // Separator
  chrome.contextMenus.create({
    id: "searchSeparator",
    parentId: "searchMenu",
    type: "separator",
    contexts: ["selection"]
  });
  
  // Another submenu under the main search menu
  chrome.contextMenus.create({
    id: "socialSearchMenu",
    parentId: "searchMenu",
    title: "Social Media",
    contexts: ["selection"]
  });
  
  chrome.contextMenus.create({
    id: "searchTwitter",
    parentId: "socialSearchMenu",
    title: "Twitter",
    contexts: ["selection"]
  });
  
  chrome.contextMenus.create({
    id: "searchReddit",
    parentId: "socialSearchMenu",
    title: "Reddit",
    contexts: ["selection"]
  });
});

This creates a hierarchical menu structure where users can first hover over “Search With…” to see the available search options, and then select a specific search engine. This organization makes your extension more usable, especially as the number of menu items grows.

Using Separators for Visual Organization

The Context Menu API supports separator items through type: "separator", which creates horizontal lines that help visually group related menu items. Separators are particularly useful when you have many menu items and want to create logical groupings that make the menu easier to navigate visually.


Dynamic Context Menus

While static context menus are created at installation time, the Context Menu API also supports dynamic menus that can be created, updated, or removed based on changing conditions. This is particularly useful for creating context-sensitive menus that adapt to the current page or user state.

Creating Menus Based on Page Content

One powerful use case for dynamic menus is creating context menus that only appear on specific pages or when certain conditions are met. While the API doesn’t directly support conditional visibility based on page content, you can achieve similar functionality by creating menus and then updating or removing them based on tab events or other triggers:

// Create a context menu that we'll show/hide based on conditions
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "specialFeature",
    title: "Special Feature",
    contexts: ["page"],
    documentUrlPatterns: ["https://*.example.com/*"]
  });
});

However, for more dynamic behavior, you might want to create menus conditionally:

// Listen for tab updates to create dynamic menus
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    // Check URL and create appropriate menus
    if (tab.url.includes('github.com')) {
      createGitHubMenus(tabId);
    } else if (tab.url.includes('youtube.com')) {
      createYouTubeMenus(tabId);
    }
  }
});

function createGitHubMenus(tabId) {
  // Remove any existing dynamic menus first
  chrome.contextMenus.remove("dynamicMenu", () => {
    chrome.contextMenus.create({
      id: "dynamicMenu",
      title: "GitHub Actions",
      contexts: ["page"],
      documentUrlPatterns: ["https://github.com/*"]
    });
  });
}

Updating Menus in Response to User Actions

You can also update context menus in real-time using chrome.contextMenus.update(). This is useful for enabling or disabling menu items, changing titles based on state, or modifying other properties:

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "toggleFeature") {
    // Toggle some state
    chrome.storage.local.get(['featureEnabled'], (result) => {
      const newState = !result.featureEnabled;
      chrome.storage.local.set({ featureEnabled: newState });
      
      // Update the menu item title to reflect new state
      chrome.contextMenus.update("toggleFeature", {
        title: newState ? "Disable Feature" : "Enable Feature"
      });
    });
  }
});

Advanced Techniques and Best Practices

Now that you understand the basics, let’s explore some advanced techniques and best practices that will help you build professional-quality context menu extensions.

Using Icons in Context Menus

Adding icons to your context menu items can significantly improve usability by making menu items more recognizable. The Context Menu API supports icons through the "icons" property, which accepts an object mapping context names to icon URLs:

chrome.contextMenus.create({
  id: "iconMenuItem",
  title: "Item with Icon",
  contexts: ["page"],
  icons: {
    "16": "images/icon16.png",
    "32": "images/icon32.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  }
});

Icons should be provided in multiple sizes to ensure they look good on different displays. Chrome will automatically select the appropriate size based on the context and display resolution. Place your icon files in your extension’s assets directory and reference them relative to the extension root.

Handling Radio Groups and Checkboxes

The Context Menu API supports radio buttons and checkboxes through the "type" property. This is useful when you want users to select one option from a group or toggle certain settings:

chrome.runtime.onInstalled.addListener(() => {
  // Radio group - only one can be selected at a time
  chrome.contextMenus.create({
    id: "searchEngineGroup",
    type: "radio",
    title: "Default Search Engine",
    contexts: ["selection"],
    checked: false
  });
  
  chrome.contextMenus.create({
    id: "engineGoogle",
    parentId: "searchEngineGroup",
    title: "Google",
    type: "radio",
    contexts: ["selection"],
    checked: true
  });
  
  chrome.contextMenus.create({
    id: "engineBing",
    parentId: "searchEngineGroup",
    title: "Bing",
    type: "radio",
    contexts: ["selection"]
  });
  
  // Checkbox - can have multiple selected
  chrome.contextMenus.create({
    id: "optionSaveHistory",
    type: "checkbox",
    title: "Save to History",
    contexts: ["selection"],
    checked: true
  });
});

When handling clicks on radio or checkbox items, the info object includes a checked property that indicates the new checked state. You can use this to update your extension’s settings accordingly.

Error Handling and Cleanup

Proper error handling is important when working with the Context Menu API. Always wrap your create(), update(), and remove() calls in error handlers, and use the callback function to check for errors:

chrome.contextMenus.create(
  {
    id: "menuItem",
    title: "My Menu Item",
    contexts: ["page"]
  },
  () => {
    if (chrome.runtime.lastError) {
      console.error("Error creating context menu:", chrome.runtime.lastError.message);
    } else {
      console.log("Context menu created successfully");
    }
  }
);

Additionally, when your extension is updated or uninstalled, you should clean up any context menus you created. This prevents orphaned menu items from remaining in Chrome:

chrome.runtime.onInstalled.addListener(() => {
  // Create menus
  createMenus();
});

chrome.runtime.onUpdateAvailable.addListener(() => {
  // Clean up old menus before update
  chrome.contextMenus.removeAll();
});

// Clean up on uninstall
chrome.runtime.setUninstallURL("https://example.com/uninstall-survey");

Working with Content Scripts

Often, your context menu click handler will need to interact with the content script running on the page to perform actions like manipulating the page content, extracting information, or communicating with the page’s JavaScript. There are several patterns for achieving this integration.

Sending Messages to Content Scripts

The most common approach is to use Chrome’s message passing API to communicate between your background script and content script:

// background.js - Handle context menu click
chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "extractLinks") {
    chrome.tabs.sendMessage(tab.id, { action: "extractLinks" }, (response) => {
      if (response && response.links) {
        // Process extracted links
        console.log("Found links:", response.links);
      }
    });
  }
});

// content.js - Listen for messages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === "extractLinks") {
    const links = Array.from(document.querySelectorAll('a'))
      .map(a => a.href);
    sendResponse({ links: links });
  }
});

Executing Script on Click

For more complex operations, you might want to execute a function directly in the context of the page:

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "highlightSelection") {
    chrome.scripting.executeScript({
      target: { tabId: tab.id },
      func: () => {
        const selection = window.getSelection();
        if (selection.rangeCount > 0) {
          const range = selection.getRangeAt(0);
          const span = document.createElement('span');
          span.style.backgroundColor = 'yellow';
          range.surroundContents(span);
        }
      }
    });
  }
});

This approach is particularly useful for page manipulation tasks like highlighting content, extracting specific elements, or modifying the page in some way.


Testing and Debugging Context Menus

Testing context menu extensions requires understanding how Chrome handles menu creation and click events. Here are some strategies for effective debugging.

Using Chrome Extension Logs

Always check chrome.runtime.lastError in your callbacks to catch and handle errors gracefully. Additionally, make use of Chrome’s extension logging capabilities:

chrome.contextMenus.create(
  {
    id: "debugMenu",
    title: "Debug Menu Item",
    contexts: ["page"]
  },
  () => {
    console.log("Menu creation result:", chrome.runtime.lastError);
  }
);

chrome.contextMenus.onClicked.addListener((info, tab) => {
  console.log("Menu clicked:", {
    menuItemId: info.menuItemId,
    selectionText: info.selectionText,
    pageUrl: info.pageUrl,
    tabId: tab.id
  });
});

Common Issues and Solutions

Several common issues can occur when developing context menu extensions. First, menu items not appearing is often caused by missing permissions, incorrect contexts configuration, or the menu being created in a context where it’s not visible. Second, click handlers not firing typically indicates that the background service worker isn’t properly set up or has crashed. Third, issues with menu item properties not updating usually stem from incorrect menu item IDs or race conditions in asynchronous operations.


Performance Considerations

When building context menu extensions that may be used frequently, performance is an important consideration. Here are some best practices for maintaining good performance.

Efficient Menu Creation

Create your context menus once during installation rather than recreating them on every extension load. Use the chrome.runtime.onInstalled event to initialize your menus, and only update them when necessary:

chrome.runtime.onInstalled.addListener(() => {
  // Create all context menus here
  createAllMenus();
});

function createAllMenus() {
  // Batch create if possible
  // Avoid recreating menus unnecessarily
}

Lazy Loading and Event Handling

If your click handler performs complex operations, consider using asynchronous patterns and proper event handling to avoid blocking the background script:

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  try {
    await handleMenuClick(info, tab);
  } catch (error) {
    console.error("Error handling menu click:", error);
    // Show error to user via notifications or content script
  }
});

Conclusion

The Chrome Context Menu API provides a powerful foundation for building extensions that enhance users’ right-click functionality with custom actions. Throughout this guide, you’ve learned how to create static and dynamic context menus, handle click events, work with hierarchical menu structures, integrate with content scripts, and implement best practices for professional-quality extensions.

As you continue developing context menu extensions, remember to consider the user experience at every step. Context menus should appear in relevant contexts, provide clear and descriptive titles, and perform their actions efficiently. With the knowledge and examples from this guide, you’re well-equipped to build powerful custom context menu extensions that significantly improve users’ browsing productivity.

Start by implementing the basic patterns demonstrated here, then expand with more advanced features as needed. The Chrome Context Menu API is well-documented and stable, making it an excellent choice for building reliable, long-lasting extensions that serve your users well.

No previous article
No next article