Chrome Extension Context Menus API Tutorial
The Chrome Context Menus API is one of the most powerful features available to extension developers, enabling you to add custom options to Chrome’s native right-click menu. When users right-click on a page, image, link, or other browser element, your extension can present context-specific actions that enhance their browsing experience. Whether you need to provide quick access to translation tools, save content to your app, create bookmarks with custom metadata, or implement developer utilities, the Context Menus API offers the flexibility and control necessary to build professional-grade extensions.
This comprehensive tutorial will walk you through building a complete Chrome extension that leverages the Context Menus API. We will cover everything from understanding the API’s architecture and capabilities to implementing advanced features like dynamic menus, context filtering, and coordination with other extension components. By the end of this guide, you will have the practical knowledge and code examples needed to create sophisticated context menu extensions that integrate seamlessly with Chrome’s user interface.
Understanding the Chrome Context Menus API
The Chrome Context Menus API, part of the chrome namespace, allows extensions to add items to the context menu that appears when users right-click on various elements within the browser. This API has been a cornerstone of extension functionality since the early days of Chrome extensions, and it continues to be an essential tool for creating intuitive user interactions. The API operates at the browser level, meaning your menu items appear regardless of which website or web application the user is interacting with, providing a consistent layer of functionality across the entire browsing experience.
The Context Menus API supports multiple contexts where your menu items can appear. The most common context is “page,” which displays your menu item when users right-click on the main content area of any webpage. However, you can also target “selection” for when users have highlighted text, “link” for right-clicks on hyperlinks, “image” for right-clicks on images, “video” and “audio” for media elements, “editable” for text input fields, and “frame” for specific iframe content. This granular control allows you to provide contextually relevant options that make sense for each type of user interaction.
One of the most powerful aspects of the Context Menus API is its support for hierarchical menus. You can create parent menu items that expand to reveal submenus, allowing you to organize complex functionality into logical groupings. This hierarchical structure keeps your extension’s menu presence clean and manageable while still providing access to extensive features. Additionally, you can specify different menu configurations for different contexts, ensuring that users only see relevant options based on what they are interacting with.
How Context Menus Work in Manifest V3
In Manifest V3, the Context Menus API requires the “contextMenus” permission in your extension’s manifest.json file. This permission grants your extension the ability to create, update, and manage context menu items. Unlike some other Chrome APIs, context menus do not require the extension to be running or have a persistent background page active at all times. Chrome manages the menu state internally and triggers your extension’s event handlers when users interact with your menu items.
The API follows an event-driven architecture where you define callback functions that Chrome invokes when users click your menu items. These callbacks receive information about the context where the click occurred, including details about the selected text, the target element, the current page URL, and the frame where the right-click happened. This contextual information allows you to implement intelligent behaviors that respond appropriately to different user scenarios.
Project Setup and Manifest Configuration
Let us begin setting up our Chrome extension project. First, create a new directory for your extension and set up the basic project structure. We will build a practical extension that demonstrates various Context Menus API capabilities, including page-level menus, link-specific actions, and selection-based functionality.
Creating the Project Structure
Create a new directory called “context-menu-extension” and set up the following files:
{
"manifest_version": 3,
"name": "Quick Actions Manager",
"version": "1.0.0",
"description": "A powerful context menu extension for quick actions",
"permissions": [
"contextMenus",
"storage",
"activeTab"
],
"background": {
"service_worker": "background.js"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
This manifest configuration declares the necessary permissions for our extension. The “contextMenus” permission is essential for creating and managing context menu items. We also include “storage” for persisting user preferences and “activeTab” for accessing information about the current tab when needed. The background service worker will handle the core context menu logic and event processing.
Setting Up the Background Script
The background script serves as the central hub for your context menu functionality. Create a background.js file that will initialize the context menus when your extension loads and handle user interactions:
// background.js - Main background service worker for context menu extension
// Store reference to created menu IDs for later use
let contextMenuIds = {};
// Initialize the extension on installation
chrome.runtime.onInstalled.addListener(() => {
createContextMenus();
console.log('Quick Actions Manager extension installed');
});
// Create all context menu items
function createContextMenus() {
// Create a parent menu item with submenus
contextMenuIds.parent = chrome.contextMenus.create({
title: 'Quick Actions',
contexts: ['page']
});
// Submenu: Save to Collection
contextMenuIds.savePage = chrome.contextMenus.create({
title: 'Save Page to Collection',
parentId: contextMenuIds.parent,
contexts: ['page'],
onclick: (info, tab) => {
savePageToCollection(info, tab);
}
});
// Submenu: Open in Reader Mode
contextMenuIds.readerMode = chrome.contextMenus.create({
title: 'Open in Reader Mode',
parentId: contextMenuIds.parent,
contexts: ['page'],
onclick: (info, tab) => {
openInReaderMode(tab);
}
});
// Separator and link-specific actions
chrome.contextMenus.create({
type: 'separator',
parentId: contextMenuIds.parent,
contexts: ['link']
});
contextMenuIds.copyLink = chrome.contextMenus.create({
title: 'Copy Link with Format',
contexts: ['link'],
onclick: (info, tab) => {
copyLinkFormatted(info);
}
});
// Selection-based menu item
contextMenuIds.searchSelection = chrome.contextMenus.create({
title: 'Search "%s" in New Tab',
contexts: ['selection'],
onclick: (info, tab) => {
searchSelection(info);
}
});
// Image-specific menu
contextMenuIds.analyzeImage = chrome.contextMenus.create({
title: 'Analyze Image',
contexts: ['image'],
onclick: (info, tab) => {
analyzeImage(info, tab);
}
});
// Page context menu for developer tools
contextMenuIds.viewPageSource = chrome.contextMenus.create({
title: 'View Page Source (Formatted)',
contexts: ['page'],
onclick: (info, tab) => {
viewFormattedSource(tab);
}
});
}
// Handler functions for menu item clicks
function savePageToCollection(info, tab) {
const pageData = {
url: tab.url,
title: tab.title,
savedAt: new Date().toISOString()
};
chrome.storage.local.get(['pages'], (result) => {
const pages = result.pages || [];
pages.push(pageData);
chrome.storage.local.set({ pages }, () => {
chrome.tabs.sendMessage(tab.id, {
action: 'showNotification',
message: 'Page saved to collection!'
});
});
});
}
function openInReaderMode(tab) {
chrome.tabs.update(tab.id, {
url: `chrome://view-source/${tab.url}`
});
}
function copyLinkFormatted(info) {
const formattedLink = `[${info.linkText || 'Link'}](${info.linkUrl})`;
navigator.clipboard.writeText(formattedLink).then(() => {
console.log('Link copied to clipboard');
});
}
function searchSelection(info) {
const query = encodeURIComponent(info.selectionText);
chrome.tabs.create({
url: `https://www.google.com/search?q=${query}`,
active: true
});
}
function analyzeImage(info, tab) {
console.log('Analyzing image:', info.srcUrl);
chrome.tabs.sendMessage(tab.id, {
action: 'showImageInfo',
srcUrl: info.srcUrl
});
}
function viewFormattedSource(tab) {
chrome.tabs.create({
url: `view-source:${tab.url}`,
active: true
});
}
This background script demonstrates several key patterns for working with the Context Menus API. We create a hierarchical menu structure with a parent item and multiple submenus, each targeting different contexts. The onclick callbacks receive information about the specific context where the right-click occurred, allowing us to implement targeted functionality for each scenario.
Advanced Context Menu Patterns
Now that we have covered the basics, let us explore more advanced patterns that will help you build professional-quality context menu extensions. These patterns include dynamic menu creation, context filtering, and coordinating with content scripts for enhanced functionality.
Dynamic Context Menus
Sometimes you need to create context menus that change based on the current page or user state. Rather than static menu definitions, you can create dynamic menus that adapt to the context. This is particularly useful for extensions that need to provide different options depending on what website the user is visiting:
// Dynamic context menu creation based on page context
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
updateContextMenusForPage(tabId, tab);
}
});
function updateContextMenusForPage(tabId, tab) {
// First, remove existing dynamic menus
chrome.contextMenus.remove('dynamic-page-menu', () => {
// Check if we're on a supported site
const url = new URL(tab.url);
if (url.hostname.includes('github.com')) {
// Add GitHub-specific context menu
chrome.contextMenus.create({
id: 'dynamic-page-menu',
title: 'GitHub Tools',
contexts: ['page'],
documentUrlPatterns: ['*://github.com/*']
});
} else if (url.hostname.includes('youtube.com')) {
// Add YouTube-specific context menu
chrome.contextMenus.create({
id: 'dynamic-page-menu',
title: 'YouTube Tools',
contexts: ['page'],
documentUrlPatterns: ['*://youtube.com/*']
});
}
});
}
// Using documentUrlPatterns for context-specific menus
chrome.contextMenus.create({
title: 'View JSON',
contexts: ['page'],
documentUrlPatterns: ['*://*.github.com/*'],
onclick: (info, tab) => {
// Handle GitHub JSON viewing
if (tab.url.includes('/blob/')) {
const jsonUrl = tab.url.replace('/blob/', '/raw/');
chrome.tabs.create({ url: jsonUrl });
}
}
});
This pattern demonstrates how to use documentUrlPatterns to conditionally show menu items based on the current website. This is incredibly powerful for creating specialized tools that only appear in contexts where they are relevant, reducing clutter in the context menu while providing targeted functionality.
Handling Menu Events with Details
When users click your context menu items, the callback receives an info object containing detailed information about the context. Understanding this information allows you to build more sophisticated interactions:
// Comprehensive onclick handler showing available info properties
chrome.contextMenus.create({
title: 'Get Page Info',
contexts: ['page'],
onclick: (info, tab) => {
// The info object contains:
// - menuItemId: The ID of the clicked menu item
// - parentMenuItemId: Parent ID if in a submenu
// - mediaType: Type of element (if applicable)
// - linkUrl: URL if clicking on a link
// - srcUrl: Source URL for images/videos
// - pageUrl: URL of the page where click occurred
// - frameId: ID of the frame where click occurred
// - frameUrl: URL of the frame where click occurred
// - selectionText: Selected text (if any)
// - editable: Boolean indicating if target is editable
console.log('Menu clicked with info:', {
menuItemId: info.menuItemId,
pageUrl: info.pageUrl,
selectionText: info.selectionText,
linkUrl: info.linkUrl,
srcUrl: info.srcUrl,
frameUrl: info.frameUrl
});
// Display info in the page via content script
chrome.tabs.sendMessage(tab.id, {
action: 'displayInfo',
info: {
url: info.pageUrl,
title: tab.title,
selection: info.selectionText,
timestamp: new Date().toISOString()
}
});
}
});
Integrating with Content Scripts
For more sophisticated functionality, you often need to coordinate between your context menu handlers and content scripts running in the page. This allows you to manipulate page content, extract information, or display UI elements based on user interactions with your context menus.
Setting Up Content Script Communication
First, add the content script to your manifest:
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}
]
}
Now create the content script that handles messages from the background script:
// content.js - Content script for context menu interactions
// Listen for messages from the background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.action) {
case 'showNotification':
showNotification(message.message);
break;
case 'showImageInfo':
showImageInfoPanel(message.srcUrl);
break;
case 'displayInfo':
displayPageInfo(message.info);
break;
case 'extractContent':
extractAndProcessContent(message.target);
break;
}
});
function showNotification(message) {
// Create a temporary notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 16px 24px;
border-radius: 8px;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function showImageInfoPanel(imageUrl) {
// Create an info panel overlay
const panel = document.createElement('div');
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
z-index: 10000;
max-width: 400px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
panel.innerHTML = `
<h3 style="margin-top: 0;">Image Analysis</h3>
<img src="${imageUrl}" style="max-width: 100%; border-radius: 8px; margin: 16px 0;">
<p><strong>Source URL:</strong></p>
<code style="word-break: break-all; font-size: 12px;">${imageUrl}</code>
<button id="close-panel" style="
margin-top: 16px;
padding: 8px 16px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
">Close</button>
`;
document.body.appendChild(panel);
panel.querySelector('#close-panel').addEventListener('click', () => {
panel.remove();
});
}
function displayPageInfo(info) {
const infoPanel = document.createElement('div');
infoPanel.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
z-index: 10000;
max-width: 300px;
font-family: monospace;
font-size: 12px;
`;
infoPanel.innerHTML = `
<h4 style="margin-top: 0;">Page Info</h4>
<pre>${JSON.stringify(info, null, 2)}</pre>
<button id="close-info" style="
margin-top: 12px;
padding: 6px 12px;
cursor: pointer;
">Close</button>
`;
document.body.appendChild(infoPanel);
infoPanel.querySelector('#close-info').addEventListener('click', () => {
infoPanel.remove();
});
}
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
Best Practices and Optimization
Building effective context menu extensions requires attention to several important best practices. These guidelines will help you create extensions that are performant, user-friendly, and compatible with Chrome’s evolving ecosystem.
Menu Organization and User Experience
When designing your context menu structure, consider the following principles. First, keep menu item titles concise but descriptive. Users should understand exactly what will happen when they click an item without needing additional explanation. Second, organize related items into submenus to prevent menu clutter. Third, use separators strategically to group related actions. Fourth, provide visual feedback when actions complete, especially for operations that may take a moment to process.
// Well-organized context menu structure example
chrome.contextMenus.create({
title: 'My Extension',
contexts: ['page'],
id: 'root'
});
// Group: Page Actions
chrome.contextMenus.create({
title: 'Page Actions',
parentId: 'root',
contexts: ['page'],
id: 'page-actions-group'
});
chrome.contextMenus.create({
title: 'Save Page',
parentId: 'page-actions-group',
contexts: ['page'],
onclick: handleSavePage
});
chrome.contextMenus.create({
title: 'Share Page',
parentId: 'page-actions-group',
contexts: ['page'],
onclick: handleSharePage
});
// Separator between groups
chrome.contextMenus.create({
type: 'separator',
parentId: 'root'
});
// Group: Selection Actions
chrome.contextMenus.create({
title: 'Selection Actions',
parentId: 'root',
contexts: ['selection'],
id: 'selection-actions-group'
});
chrome.contextMenus.create({
title: 'Search This',
parentId: 'selection-actions-group',
contexts: ['selection'],
onclick: handleSearchSelection
});
chrome.contextMenus.create({
title: 'Copy Formatted',
parentId: 'selection-actions-group',
contexts: ['selection'],
onclick: handleCopyFormatted
});
Performance Considerations
Context menu creation is relatively lightweight, but there are performance considerations to keep in mind. Avoid creating hundreds of menu items, as this can slow down the right-click menu appearance. Use documentUrlPatterns to restrict menus to relevant pages rather than checking URLs in your onclick handlers. When possible, use the icons property to provide visual cues that help users identify your extension’s menu items quickly.
// Performance-optimized menu creation
// Use documentUrlPatterns to avoid unnecessary menu items
chrome.contextMenus.create({
title: 'GitHub: View Raw File',
contexts: ['page'],
documentUrlPatterns: [
'*://github.com/*/*/blob/*',
'*://github.com/*/*/*/blob/*'
],
onclick: (info, tab) => {
const rawUrl = tab.url.replace('/blob/', '/raw/');
chrome.tabs.create({ url: rawUrl });
}
});
// Remove unused menus when no longer needed
chrome.contextMenus.remove('obsolete-menu-item', () => {
console.log('Obsolete menu item removed');
});
// Update menus based on user preferences
chrome.storage.onChanged.addListener((changes, areaName) => {
if (changes.userPreferences) {
updateMenusBasedOnPreferences(changes.userPreferences.newValue);
}
});
Error Handling and Edge Cases
Robust error handling is essential for maintaining a positive user experience. Your context menu handlers should gracefully handle scenarios where required permissions are missing, the active tab has been closed, or external resources are unavailable:
// Comprehensive error handling for context menu handlers
chrome.contextMenus.create({
title: 'Process Page Content',
contexts: ['page'],
onclick: async (info, tab) => {
try {
// Validate tab exists and is accessible
if (!tab || !tab.id) {
throw new Error('No active tab available');
}
// Check if we can access the tab
try {
await chrome.tabs.get(tab.id);
} catch (tabError) {
throw new Error('Cannot access current tab. Try refreshing the page.');
}
// Send message to content script with timeout
const response = await chrome.tabs.sendMessage(tab.id, {
action: 'processContent'
}).timeout(5000);
if (response.success) {
console.log('Content processed successfully');
} else {
console.warn('Content processing returned no results');
}
} catch (error) {
console.error('Error processing page:', error.message);
// Show user-friendly error notification
chrome.tabs.sendMessage(tab.id, {
action: 'showError',
message: error.message
}).catch(() => {
// Fallback if content script is not available
console.error('Could not display error in page');
});
}
}
});
Testing and Debugging
Developing context menu extensions requires thorough testing across different scenarios. Chrome provides developer tools that help you debug your extension’s context menu functionality effectively.
Using Chrome Developer Tools for Extension Debugging
To test your context menu extension, navigate to chrome://extensions/ in your Chrome browser. Enable Developer mode in the top right corner, then click “Load unpacked” and select your extension’s directory. Any errors in your background script will appear in the console. You can also right-click on your extension’s service worker and select “Inspect views” to open the background script’s developer tools.
When testing, verify that your menu items appear in the correct contexts, that onclick handlers fire correctly, that content script communication works as expected, and that your extension handles edge cases gracefully. Test on multiple websites and with various types of content to ensure broad compatibility.
Conclusion
The Chrome Context Menus API provides a powerful foundation for building intuitive, context-aware extensions that enhance users’ browsing experiences. Throughout this tutorial, we have covered the essential concepts of menu creation, advanced patterns for dynamic and context-specific menus, content script integration for sophisticated functionality, and best practices for building professional-quality extensions.
By following the examples and patterns presented in this guide, you now have the knowledge needed to create context menu extensions that are well-organized, performant, and user-friendly. The key to success is understanding your users’ needs and providing targeted functionality that appears in the right context at the right time.
As you continue developing extensions, remember to test thoroughly across different scenarios, handle errors gracefully, and consider performance implications when designing your menu structures. With these skills and best practices in mind, you are well-equipped to build powerful context menu extensions that provide genuine value to Chrome users.
Related Articles
- Chrome Extension Permissions Explained - Understand which permissions your extension needs and how to request them properly
- Chrome Extension Popup Design Best Practices - Learn how to design effective popup interfaces for your extensions
- Chrome Extension OAuth2 Authentication Guide - Implement secure authentication in your Chrome extensions
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.