Context Menu Contexts and Target Filters

Show a context menu item only where it applies — contexts, documentUrlPatterns and targetUrlPatterns in chrome.contextMenus, and why filtering at registration beats hiding at click time.

Published September 18, 2026 Updated September 18, 2026 7 min read
Table of Contents

A context menu item that appears everywhere and does nothing on most right-clicks trains users to ignore your extension. chrome.contextMenus offers three filters that decide where an item appears — the kind of thing clicked, the page it is on, and the URL of a link or image — and they are evaluated by the browser before the menu opens, with no worker round trip. Getting them right is most of the difference between a menu item that feels native and one that feels like clutter. This guide is part of context menus and right-click actions.

The three filters

How the browser decides whether to show an itemThree filters applied in sequence: the context type of the click, the document URL pattern of the page, and the target URL pattern of the link, image or media element.contextswhat was right-clickedselection, link, image, page…documentUrlPatternswhich page it happened onmatch patterns on the page URLtargetUrlPatternsthe link / image / media URLonly for link, image, video, audioItem shownall filters passno worker wake needed
All three are evaluated synchronously in the browser — your service worker is not involved until the user clicks.

Step-by-step

1. Pick the narrowest context

1chrome.runtime.onInstalled.addListener(() => {
2  chrome.contextMenus.create({
3    id: "define-selection",
4    title: "Define “%s”",
5    contexts: ["selection"],
6  });
7});

Execution context: the service worker, inside onInstalled — menu items persist across restarts, so creating them on every startup produces “duplicate id” errors. %s is replaced by the browser with the selected text, truncated, which makes the item self-describing without any code.

The available contexts are all, page, frame, selection, link, editable, image, video, audio, launcher, browser_action/action and page_action. all is almost never right; it is the context that produces clutter.

2. Restrict by page with documentUrlPatterns

1chrome.contextMenus.create({
2  id: "save-to-reader",
3  title: "Save to Reader",
4  contexts: ["page", "link"],
5  documentUrlPatterns: ["https://*/*", "http://*/*"],    // not on chrome://, file:// or PDFs
6});

Execution context: the service worker. Match patterns use the same syntax as content_scripts.matches. Excluding non-web pages here means the item never appears where the action cannot work — the restricted pages listed in handling restricted URLs and tab permissions.

3. Restrict by target with targetUrlPatterns

For link, image, video and audio contexts, the filter can look at the element’s URL rather than the page’s.

 1chrome.contextMenus.create({
 2  id: "open-issue",
 3  title: "Open issue in tracker",
 4  contexts: ["link"],
 5  targetUrlPatterns: ["https://github.com/*/issues/*", "https://gitlab.com/*/-/issues/*"],
 6});
 7
 8chrome.contextMenus.create({
 9  id: "save-image",
10  title: "Save image to library",
11  contexts: ["image"],
12  targetUrlPatterns: ["https://*/*.jpg", "https://*/*.png", "https://*/*.webp"],
13});

Execution context: the service worker. The issue item appears only when right-clicking a link to an issue, on any page — something a page-level filter cannot express. Match patterns cannot match query strings, so a URL like …/image?format=png needs a broader pattern and a check in the click handler.

4. Combine filters rather than checking in the handler

1// Worse: shows everywhere, bails at click time
2chrome.contextMenus.onClicked.addListener((info) => {
3  if (info.menuItemId !== "open-issue") return;
4  if (!/\/issues\/\d+/.test(info.linkUrl)) return;   // the user already saw a useless item
5  openInTracker(info.linkUrl);
6});

Execution context: the service worker. A handler-side check still has a job — validating the URL precisely — but it cannot hide the item. Anything expressible as a match pattern belongs in the filter, so the user never sees an item that will do nothing.

5. Change filters at runtime when the user’s configuration changes

1async function syncMenuToSettings({ enabledOrigins }) {
2  await chrome.contextMenus.update("save-to-reader", {
3    documentUrlPatterns: enabledOrigins.length ? enabledOrigins.map((o) => `${o}/*`) : ["https://example.invalid/*"],
4  });
5}

Execution context: the service worker, called from a storage.onChanged listener. update changes an existing item in place, which is cheaper and less error-prone than removing and recreating it. An empty pattern list is rejected, hence the placeholder that matches nothing real.

Which filter answers which questionFive common menu requirements mapped to the filter that expresses each, with notes on limits.RequirementFilterLimitOnly on selected textcontexts: selectionNoneOnly on web pagesdocumentUrlPatternsMatch-pattern syntaxOnly on links to one sitetargetUrlPatternsNo query stringsOnly in text inputscontexts: editableNoneOnly when a setting is onupdate() / visibleNeeds a worker write
If a requirement fits the first two columns, it belongs in a filter — not in the click handler.

Visibility, enablement and the cost of each

Beyond the filters, an item has visible and enabled properties. They are the right tool for state the filters cannot express — “only when signed in”, “only when the feature is turned on” — and they cost more, because changing them requires a worker to be running and to call update.

1chrome.storage.onChanged.addListener(async (changes, area) => {
2  if (area !== "local" || !("accessToken" in changes)) return;
3  const signedIn = !!changes.accessToken.newValue;
4  await chrome.contextMenus.update("save-to-reader", { visible: signedIn });
5});

Execution context: the service worker, registered at the top level. visible: false removes the item from the menu entirely; enabled: false shows it greyed out. Greyed out is right when the user can fix the condition and should know the feature exists; hidden is right when the item is irrelevant.

What neither can do is change per click based on the page’s contents — there is no hook that runs before the menu opens. Chrome’s contextMenus API has no onShown event; Firefox’s menus.onShown does, and it is the one place where content-dependent menus are possible without guesswork. The Chrome pattern for page-dependent menus — updating on tab activation and navigation — is in dynamic context menu generation based on page content.

Filter, visible, or enabled?A decision tree choosing between a registration-time filter, a visible toggle and an enabled toggle based on what the condition depends on.What does the condition depend on?What was clicked, or the URLFiltercontexts / *UrlPatternsEvaluated by the browserno workerExtension state, irrelevant when offvisible: falsehide entirelyUpdate on state changestorage.onChangedState the user can fixenabled: falsegreyed outTitle explains why"Sign in to save"
Filters are free at menu-open time; visible and enabled cost a worker wake whenever the condition changes.

Cross-browser variation

  • Chrome / Edge: chrome.contextMenus supports all three filters, visible and enabled. There is no onShown event, so content-dependent items must be updated ahead of the right-click.
  • Firefox: browser.menus (with contextMenus as an alias) supports the same filters plus extra contexts such as tab and bookmark, and menus.onShown/menus.refresh() for per-click updates.
  • Safari: supports contextMenus with a smaller set of contexts; targetUrlPatterns support has been inconsistent. Prefer documentUrlPatterns and handler-side checks for Safari builds.
  • All three: items are limited per extension — Chrome caps top-level items at chrome.contextMenus.ACTION_MENU_TOP_LEVEL_LIMIT for the action menu, and multiple page-menu items are grouped under the extension’s name automatically.

Verification

  1. Right-click selected text, a link, an image and empty page space; confirm each item appears only where intended.
  2. Confirm the registered filters from the service worker console:
1chrome.contextMenus.create({ id: "probe", title: "probe", contexts: ["page"] }, () => {
2  console.log(chrome.runtime.lastError?.message ?? "created");
3  chrome.contextMenus.remove("probe");
4});

Execution context: the service worker console. There is no getAll for menus in Chrome, so the practical audit is to log your own creation calls; a “duplicate id” error here reveals an item created twice.

  1. Visit chrome://extensions and right-click; items restricted by documentUrlPatterns to web pages should be absent.
  2. Toggle the controlling setting and confirm visible changes take effect on the next right-click.

FAQ

Can I filter by the selected text’s content?

No — there is no pattern filter for selection text. Show the item for any selection and validate in the handler, or on Firefox use menus.onShown to update the title with the selection before the menu renders.

Why does my item appear under a sub-menu with my extension’s name?

Because the extension registered more than one item for that context. Chrome groups them automatically. Registering a single item keeps it at the top level.

Do filters work for the toolbar action’s menu?

The action context has its own menu and ignores URL patterns. Use visible for conditional items there.

Is there a limit on how many items I can register?

There is no small hard cap for page-context items, but every extension’s items share one right-click menu with the browser’s own entries and other extensions’. More than two or three items for any single context is a design problem regardless of what the API allows — group the rest under a parent item, as described in nested and radio context menu items.

Other UI/UX Patterns & Interactive Components Resources