Context Menus in Firefox and Safari
Port Chrome context menus to Firefox and Safari: the menus namespace, onShown and refresh, extra contexts like tab and bookmark, icons on items, Safari's supported contexts, and one portable registration layer.
Table of Contents
- Where the APIs differ
- Step-by-step: one portable menu layer
- 1. Pick the namespace once
- 2. Define items as data, filtered by capability
- 3. Handle clicks once for all engines
- 4. Update items as the menu opens in Firefox
- 5. Use getTargetElement for element-aware actions in Firefox
- 6. Degrade gracefully in Safari
- 7. Keep one test per engine
- 8. Scope Firefox items to the right view types
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
Context menu code written for Chrome runs in Firefox and mostly runs in Safari — but “mostly” hides useful differences. Firefox’s menus API is a superset with features Chrome lacks: an event when the menu opens, the ability to refresh items while it is shown, menus on tabs and bookmarks, and icons on individual items. Safari supports a narrower set of contexts and options. A portable extension uses the common core everywhere and adds Firefox enhancements behind feature detection, while degrading gracefully where Safari lacks a context. This guide maps the differences. It belongs to context menus and right-click actions.
Where the APIs differ
All three engines implement create, update, remove, removeAll and onClicked, with item types normal, checkbox, radio and separator, and the core contexts — page, selection, link, image, video, audio, editable, frame and action. Chrome calls the namespace contextMenus; Firefox calls it menus and aliases contextMenus to it. Firefox adds the tab context (right-click on a tab strip entry), bookmark and tools_menu, the menus.onShown/onHidden events with menus.refresh(), per-item icons, viewTypes and menus.getTargetElement() to find the clicked DOM element from a content script. Safari supports the core page contexts but not every option; some properties are ignored.
Step-by-step: one portable menu layer
1. Pick the namespace once
1// menus.js
2export const menus = globalThis.browser?.menus ?? globalThis.chrome?.contextMenus;
3export const isFirefoxMenus = typeof globalThis.browser?.menus?.onShown?.addListener === "function";
Execution context: the background in each engine. Using one reference keeps the rest of the code identical. Detecting onShown rather than the browser name means any engine that adds it later benefits automatically.
2. Define items as data, filtered by capability
1const ITEMS = [
2 { id: "save-link", title: "Save link to Readable", contexts: ["link"] },
3 { id: "save-sel", title: "Save selection", contexts: ["selection"] },
4 { id: "save-tab", title: "Save this tab", contexts: ["tab"], firefoxOnly: true },
5 { id: "save-image", title: "Save image", contexts: ["image"], icons: { 16: "icons/img-16.png" } },
6];
7
8export async function createAll() {
9 await menus.removeAll();
10 for (const { firefoxOnly, icons, ...item } of ITEMS) {
11 if (firefoxOnly && !isFirefoxMenus) continue;
12 menus.create(isFirefoxMenus && icons ? { ...item, icons } : item);
13 }
14}
Execution context: the background, called from onInstalled and onStartup. Unsupported contexts or properties can make create throw or the item silently not appear, so filter them per engine. Firefox-only items like the tab context add value there without breaking other builds. Icons are omitted outside Firefox, where the property is not supported.
3. Handle clicks once for all engines
1menus.onClicked.addListener(async (info, tab) => {
2 switch (info.menuItemId) {
3 case "save-link": return saveUrl(info.linkUrl, tab);
4 case "save-sel": return saveText(info.selectionText, tab);
5 case "save-image": return saveUrl(info.srcUrl, tab);
6 case "save-tab": return saveUrl(tab.url, tab); // Firefox: tab is the right-clicked tab
7 }
8});
Execution context: the background, registered at the top level. info fields — linkUrl, selectionText, srcUrl, pageUrl, frameId — are the same across engines. For the Firefox tab context, the tab argument is the tab that was right-clicked in the tab strip, which may not be the active one.
4. Update items as the menu opens in Firefox
1if (isFirefoxMenus) {
2 browser.menus.onShown.addListener(async (info, tab) => {
3 if (info.contexts.includes("link")) {
4 const saved = await isSaved(info.linkUrl);
5 browser.menus.update("save-link", { title: saved ? "Already saved — open in Readable" : "Save link to Readable" });
6 browser.menus.refresh();
7 }
8 });
9}
Execution context: the Firefox background. onShown reports which contexts the menu was opened for and lets you adjust titles, visibility or checked state with the real target in hand — then refresh() redraws it. Keep the handler fast; the menu is already open. Chrome has no equivalent, so Chrome builds should show a title that works without knowing the target, or update proactively as described in checkbox context menu items that reflect state.
5. Use getTargetElement for element-aware actions in Firefox
1// content script (Firefox)
2browser.runtime.onMessage.addListener((msg) => {
3 if (msg.type !== "describe-target") return;
4 const el = browser.menus.getTargetElement(msg.targetElementId);
5 return Promise.resolve({ tag: el?.tagName, text: el?.textContent?.slice(0, 200) });
6});
7// background: send info.targetElementId from onClicked to the content script in info.frameId
Execution context: a Firefox content script. info.targetElementId in onClicked identifies the exact element right-clicked, and getTargetElement resolves it in the content script. In Chrome, approximate it by recording the last contextmenu event’s target in the content script and asking for it after the click.
6. Degrade gracefully in Safari
Test each item in Safari: some contexts (such as action in older versions) and options may be unsupported, and items requiring them should be omitted rather than appearing in the wrong place. Keep Safari’s menu to the core actions and point users to the popup for the rest.
7. Keep one test per engine
Automated context-menu testing is limited, but you can test the registration layer: run createAll() with a mock menus object per engine profile and assert which items are created with which properties. Then do a short manual check per engine before release.
8. Scope Firefox items to the right view types
1browser.menus.create({
2 id: "inspect-panel-item",
3 title: "Copy item ID",
4 contexts: ["all"],
5 viewTypes: ["sidebar", "popup"],
6 documentUrlPatterns: [browser.runtime.getURL("*")],
7});
Execution context: the Firefox background. viewTypes limits an item to extension views such as the sidebar or popup, so a command meant for your own UI never leaks into web pages. Combined with documentUrlPatterns restricted to the extension’s own origin, it gives your sidebar a native right-click menu. Chrome builds should skip this item and offer the same action as a button in the side panel instead.
Common mistakes
- Using Firefox-only contexts unconditionally.
createthrows or items vanish in Chrome. - Relying on
onShowneverywhere. Chrome has no pre-show hook. - Assuming the
tabargument is the active tab. For tab-strip menus it is the clicked tab. - Icons on items in Chrome. Unsupported; omit them there.
- Not testing Safari. Some contexts silently don’t appear.
Cross-browser variation
This guide is the variation: Chrome and Edge provide the shared core; Firefox adds onShown, refresh, extra contexts, icons and target elements; Safari supports the core page contexts with fewer options. The same item data and click handler serve all three.
Verification
- Load each build and confirm the expected items appear in each context.
- In Firefox, right-click a saved link and confirm the title changes via
onShown. - In Firefox, right-click a tab in the tab strip and confirm “Save this tab” acts on that tab.
- In Chrome and Safari, confirm no errors from unsupported properties in the background console.
FAQ
Can I add items to the browser’s tab strip menu in Chrome?
No. Chrome does not expose a tab context to extensions.
Do Firefox menus items need a different permission?
Firefox accepts menus or contextMenus in permissions; both grant the API.
Can I show a menu without the user right-clicking?
No. Context menus open only on user action.
Does menus.refresh() work if the menu is already closed?
No. It only redraws a menu that is currently open, and it resolves without effect otherwise. Call it only from inside onShown after async work finishes, and check that the menu instance is still the one you started with — track an instance counter incremented in onShown and onHidden, and skip the refresh when the counter has moved on.
Should I ship one build for Chrome and Firefox?
You can, if the code uses feature detection as shown and the manifest differences are handled by your build. Many teams still produce separate packages so each store gets only the keys it understands.
Related
- Context menu contexts and target filters — contexts in depth.
- Checkbox context menu items that reflect state — Chrome’s proactive update pattern.
- Porting a Chrome extension to Firefox — the wider port.
- Context menus and right-click actions — the parent topic.