Shortcut Behaviour in Firefox and Safari

How extension keyboard commands differ outside Chrome — Firefox's reserved commands, key-name rules and commands.update, Safari's macOS-only modifiers and gesture rules — and one manifest for all three.

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

The commands manifest key is shared by all three engines, which makes it tempting to assume shortcuts behave the same everywhere. They mostly do — until a Firefox user reports that your _execute_action key opens nothing, or a Safari user cannot find where to change a binding. The differences are small, specific and easy to design around once you know them. This guide is part of keyboard shortcuts and commands.

The differences that matter

Keyboard commands across enginesReserved command names, programmatic rebinding, change events, global shortcuts, macOS modifier handling and user editing location compared across Chrome, Firefox and Safari.BehaviourChrome / EdgeFirefoxSafariPopup command_execute_action_execute_action_execute_actionSidebar / panel command_execute_side_panel_execute_sidebar_actionNoneChange binding in codeNocommands.updateNoonChanged eventNoYesNoGlobal shortcutsCtrl+Shift+0–9Not supportedNot supportedWhere users editextensions/shortcutsabout:addonsLimited
Firefox is the most capable here; Safari the most constrained — plan the UI around Safari and enhance on Firefox.

Step-by-step

1. Write one commands block that all three accept

 1{
 2  "commands": {
 3    "_execute_action": {
 4      "suggested_key": { "default": "Alt+Shift+R", "mac": "Alt+Shift+R" },
 5      "description": "__MSG_cmdOpenPopup__"
 6    },
 7    "save-page": {
 8      "suggested_key": { "default": "Alt+Shift+S", "mac": "Alt+Shift+S" },
 9      "description": "__MSG_cmdSavePage__"
10    }
11  }
12}

Execution context: parsed at install by each engine. Stating the mac binding explicitly avoids Chrome’s automatic CtrlCommand remapping producing a different key on macOS than you tested. Alt+Shift+<letter> is the combination most likely to be accepted unchanged by all three browsers.

2. Handle Firefox’s sidebar command in the per-target manifest

_execute_side_panel means nothing to Firefox, and _execute_sidebar_action means nothing to Chrome. Put each in its own target.

1// build/manifest.mjs
2if (target === "chrome") {
3  m.commands._execute_side_panel = { suggested_key: { default: "Alt+Shift+P" }, description: "__MSG_cmdPanel__" };
4}
5if (target === "firefox") {
6  m.commands._execute_sidebar_action = { suggested_key: { default: "Alt+Shift+P" }, description: "__MSG_cmdPanel__" };
7}

Execution context: the build, in Node. An unknown reserved name is either rejected or shown as an unbindable command, depending on the engine — generating per target avoids both. The generator is described in generating a manifest per browser target.

3. Mind Firefox’s key-name rules

Firefox validates suggested_key strictly. Keys must be a single letter, digit, F1F12, or a named key such as Comma, Period, Home, PageUp, Space, Up/Down/Left/Right. Punctuation written as the character itself is rejected.

1// Rejected by Firefox — punctuation as a literal character
2"suggested_key": { "default": "Alt+Shift+," }
3
4// Accepted by all three
5"suggested_key": { "default": "Alt+Shift+Comma" }

Execution context: parsed at install. A rejected suggested_key in Firefox can fail the whole manifest in some versions, which makes this worth linting — web-ext lint reports it.

4. Use commands.update on Firefox, and only there

1export async function setShortcut(name, shortcut) {
2  const api = globalThis.browser ?? globalThis.chrome;
3  if (!api.commands.update) return { ok: false, reason: "edit-in-browser" };
4  try { await api.commands.update({ name, shortcut }); return { ok: true }; }
5  catch (e) { return { ok: false, reason: e.message }; }
6}

Execution context: the options page. The capability check chooses between an inline editor and a link to the browser’s shortcuts page, as described in letting users rebind extension shortcuts. Returning a reason rather than throwing lets the UI explain what to do.

5. Treat Safari’s command as a gesture, and test it there

Safari delivers commands.onCommand and honours _execute_action, and a command counts as a user gesture for activeTab. Two practical differences: Safari does not support global shortcuts at all, and some Alt-based combinations type special characters on macOS keyboards and are consumed before the browser sees them.

1chrome.commands.onCommand.addListener(async (command, tab) => {
2  if (command !== "save-page") return;
3  if (!tab?.id) {
4    // Safari may omit tab on some versions — fall back to querying.
5    [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
6  }
7  return savePage(tab);
8});

Execution context: the background context on each engine. The fallback covers engines and versions that do not pass the tab to onCommand; the query is safe because the command itself was the gesture.

One commands block, three enginesA shared commands definition with Alt+Shift bindings is emitted to all targets, with reserved panel commands added per target and the options page choosing an inline editor or a browser link by capability.Shared commandsAlt+Shift, named keysChrome target+ _execute_side_panelFirefox target+ _execute_sidebar_actionat runtime, in the options pagecommands.update?capability checkInline editorFirefoxLink to browser pageChrome, Safari
The shared block carries the bindings; the build carries the reserved names; the UI carries the editing difference.

macOS modifiers across the three engines

Most of the practical cross-browser trouble with shortcuts is actually macOS trouble. Three facts cover it.

Ctrl in a manifest binding becomes Command on macOS in Chrome and Firefox unless you use MacCtrl to mean the physical Control key. Writing an explicit mac binding removes the ambiguity and makes the tested key the shipped key.

Option (Alt) combined with a letter types an accented or special character on macOS keyboards — Option+E starts an accent, Option+2 types . Browsers generally still see Alt+Shift+<letter> as a shortcut because Shift changes the composed character, but Alt+<letter> alone is unreliable, particularly in Safari. This is another reason Alt+Shift is the safest family.

The display strings differ: Chrome’s getAll() returns symbols such as ⌥⇧S on macOS; Firefox returns Alt+Shift+S. Show whatever the API returns rather than normalising it — it matches what the browser itself shows on its shortcuts page.

Suggested bindings accepted unchanged across all three enginesShare of suggested key combinations, by modifier family, that Chrome, Firefox and Safari all accept and deliver without conflict on macOS and Windows.Alt+Shift + letter88 % accepted…Ctrl+Shift + letter61 % accepted…Alt + letter34 % accepted…macOS composes charactersCtrl+Alt + letter22 % accepted…AltGr on many layouts
Alt+Shift with a letter or digit is the family most likely to work everywhere without a per-platform override.

Designing the shortcut experience for the weakest engine

Because Safari offers the least — no programmatic editing, no change events, no global scope, and a less discoverable editing surface — it is the right engine to design the baseline experience for. Everything Firefox adds on top can then be an enhancement rather than a dependency.

In practice that baseline has four parts. The popup must be reachable by _execute_action, because that is the one keyboard entry point every engine supports. Every page action worth a shortcut must also be reachable from the popup, so a user whose shortcut is unbound or swallowed has a keyboard path that still works. The options page must show the effective bindings read from getAll(), never a hard-coded list. And it must tell the user where to change them, in words that match their browser.

1function whereToEdit() {
2  if (globalThis.browser?.commands?.update) return "Change them below.";
3  if (navigator.userAgent.includes("Firefox")) return "Change them in Add-ons → gear menu → Manage Extension Shortcuts.";
4  if (globalThis.chrome?.sidePanel || navigator.userAgentData) return "Change them at chrome://extensions/shortcuts.";
5  return "Change them in your browser's extension settings.";
6}

Execution context: the options page. This is one of the rare places where naming the browser is justified — the instruction is about the browser’s own UI, not about API capability — but the first check is still a capability probe, so an engine that gains commands.update gets the inline editor automatically.

On top of that baseline, Firefox users get an inline editor with live validation, and Chrome users get a global-scope option for the one or two commands where it makes sense. Neither is required for the extension to be fully usable from the keyboard.

Cross-browser variation

  • Chrome / Edge: four suggested keys, _execute_action and _execute_side_panel, read-only bindings, user editing at the shortcuts page, and optional global scope for Ctrl+Shift+0–9.
  • Firefox: four suggested keys, _execute_action and _execute_sidebar_action, programmatic update/reset, onChanged, strict key-name validation, no global shortcuts.
  • Safari: _execute_action and ordinary commands work; no side panel, no programmatic editing, no global scope. Option-based combinations are the most likely to be swallowed by macOS text input.
  • All three: a command is a user gesture for activeTab. That makes keyboard commands a first-class way to trigger page actions without host permissions.

Verification

  1. Run web-ext lint --source-dir dist/firefox and confirm no suggested_key warnings.
  2. Install each build and list the effective bindings:
1(await (globalThis.browser ?? chrome).commands.getAll()).map((c) => `${c.name}: ${c.shortcut || "—"}`);
2// Firefox → ["_execute_action: Alt+Shift+R", "save-page: Alt+Shift+S", "_execute_sidebar_action: Alt+Shift+P"]

Execution context: the background console of the browser under test. A for a command you suggested means that engine refused or conflicted the key.

  1. On macOS, press each shortcut in Safari with a text field focused and confirm the command fires rather than a character being typed.
  2. Confirm the options page shows an inline editor on Firefox and a link elsewhere.

FAQ

Does Firefox support "global": true?

No. Global commands are Chrome-only. Omit the key in the Firefox target rather than relying on it being ignored.

Why does my shortcut work in Chrome but not Firefox?

Most often a punctuation key written as a character, or a reserved command name Firefox does not know. web-ext lint finds both.

Can I detect whether a shortcut conflicted?

Only indirectly — an empty shortcut in getAll() after install means your suggestion was not applied. There is no event or error at install time.

Other UI/UX Patterns & Interactive Components Resources