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.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

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.

Context menu features by engineComparison of namespace, onShown/refresh, extra contexts, item icons and getTargetElement across Chrome, Firefox and Safari.FeatureChrome / EdgeFirefoxSafariNamespacecontextMenusmenus (+ alias)contextMenusonShown + refreshNoYesNotab / bookmark contextsNoYesNoItem iconsNoYesNogetTargetElementNoYesNoCore contextsYesYesMost
Write for the shared core; add Firefox's extras behind detection.

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.

One item list, three enginesA shared list of menu items is filtered by capability: Chrome and Safari receive core items, Firefox additionally receives tab-context items and icons, and Firefox uses onShown to update labels as the menu opens.ITEMSshared definitionCapability filtercontexts, iconscreate()per engineengine-specific behaviourChrome / Edgecore itemsFirefox+ tab, icons, onShownSafaricore page contexts
The data is shared; only the filter and the extras differ.

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.

Firefox onShown versus ChromeIn Firefox, right-clicking a link fires onShown, the background checks whether the link is saved, updates the title and refreshes the open menu; in Chrome the menu opens with its last title and no hook runs.UserFirefox backgroundMenuright-click linkonShown({contexts:['link'], linkUrl})isSaved(linkUrl) → trueupdate title + refresh()"Already saved — open"
Firefox lets the menu adapt to its target; Chrome shows what you set beforehand.

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. create throws or items vanish in Chrome.
  • Relying on onShown everywhere. Chrome has no pre-show hook.
  • Assuming the tab argument 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

  1. Load each build and confirm the expected items appear in each context.
  2. In Firefox, right-click a saved link and confirm the title changes via onShown.
  3. In Firefox, right-click a tab in the tab strip and confirm “Save this tab” acts on that tab.
  4. 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.

Other UI/UX Patterns & Interactive Components Resources