Letting Users Rebind Extension Shortcuts

Give users control of extension shortcuts — linking to the browser's shortcuts page, Firefox's commands.update, detecting changes, and keeping hints in your UI accurate.

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

Whatever default keys you choose, some users will need different ones: a conflict with a web application they live in, a keyboard layout where your letter is awkward, or an accessibility need. Chrome deliberately does not let extensions change bindings themselves — the user does it on the browser’s own shortcuts page — while Firefox exposes commands.update for an in-extension editor. A good rebinding experience works within both models and keeps every hint in your UI truthful afterwards. This guide is part of keyboard shortcuts and commands.

Who is allowed to change a binding

Rebinding capabilities by engineWhether the extension can read bindings, change them itself, reset them, and where users change them in Chrome, Firefox and Safari.CapabilityChrome / EdgeFirefoxSafariRead bindingscommands.getAllcommands.getAllcommands.getAllChange a bindingNo — user onlycommands.updateNoReset to defaultNocommands.resetNoUser edits atchrome://extensions/short…about:addons → gearSystem settingsChange eventNone — pollcommands.onChangedNone
Every engine lets you read; only Firefox lets you write — which shapes what an in-extension settings screen can offer.

Step-by-step

1. Show the current bindings in your settings

 1async function renderShortcuts(list) {
 2  const commands = await chrome.commands.getAll();
 3  list.replaceChildren(...commands.map((c) => {
 4    const row = document.createElement("li");
 5    const label = document.createElement("span");
 6    label.textContent = c.name === "_execute_action" ? "Open Reader" : c.description;
 7    const key = document.createElement("kbd");
 8    key.textContent = c.shortcut || "Not set";
 9    row.append(label, key);
10    return row;
11  }));
12}

Execution context: the options page. _execute_action has an empty description in some versions, so give it a label yourself. textContent keeps descriptions — which come from your own locale files but pass through the browser — out of the markup path.

1const isFirefox = typeof browser !== "undefined" && !!browser.commands?.update;
2
3document.querySelector("#edit-shortcuts").addEventListener("click", async () => {
4  if (isFirefox) return openInlineEditor();
5  await chrome.tabs.create({ url: "chrome://extensions/shortcuts" });
6});

Execution context: the options page. Probing for commands.update rather than sniffing the user agent picks the right path on Firefox and on any future Chromium browser that adds the API — the approach in feature detection instead of browser sniffing.

3. Offer an in-extension editor on Firefox

 1async function bind(name, shortcut) {
 2  try {
 3    await browser.commands.update({ name, shortcut });
 4    return { ok: true };
 5  } catch (err) {
 6    return { ok: false, reason: err.message };   // e.g. "Type error for parameter detail"
 7  }
 8}
 9
10function captureShortcut(input) {
11  input.addEventListener("keydown", (e) => {
12    e.preventDefault();
13    const parts = [e.ctrlKey && "Ctrl", e.altKey && "Alt", e.shiftKey && "Shift", e.metaKey && "Command"].filter(Boolean);
14    if (/^[A-Z0-9]$/i.test(e.key)) input.value = [...parts, e.key.toUpperCase()].join("+");
15  });
16}

Execution context: the options page on Firefox. commands.update validates the combination and rejects anything the browser will not accept; surface the rejection next to the field rather than silently keeping the old binding. A shortcut must include at least one modifier other than Shift.

4. Detect changes the user made elsewhere

Firefox fires commands.onChanged. Chrome has no event, so re-read when your UI becomes visible.

1if (browser?.commands?.onChanged) {
2  browser.commands.onChanged.addListener(() => renderShortcuts(list));
3}
4document.addEventListener("visibilitychange", () => {
5  if (document.visibilityState === "visible") renderShortcuts(list);
6});

Execution context: the options page. The visibilitychange re-read covers the common Chrome flow exactly: the user clicks “edit”, changes the key on the shortcuts page, and comes back to your tab — which becomes visible and refreshes.

5. Keep every hint in the UI truthful

Hints like “Press Alt+Shift+S to save” appear in the popup, onboarding and tooltips. Hard-coding them guarantees they will be wrong for some users.

1export async function shortcutFor(name) {
2  const c = (await chrome.commands.getAll()).find((x) => x.name === name);
3  return c?.shortcut || null;
4}
5
6// popup.js
7const key = await shortcutFor("save-page");
8saveButton.title = key ? `Save this page (${key})` : "Save this page";

Execution context: any extension page. When the user has removed a binding, the hint disappears rather than advertising a key that does nothing.

A Chrome user rebinding a shortcutThe options page links to the browser's shortcuts page, the user changes the binding there, and on returning to the tab the options page re-reads commands and updates every hint.UserOptions pagechrome://extensions/shortcu…commands APIclick "Edit shortcuts"tabs.create(shortcuts page)set save-page to Alt+Shift+Kswitch back to the options tabvisibilitychange → getAll()save-page: Alt+Shift+K
No event tells the extension the binding changed — the visibility change is the trigger.

Helping users choose a good key

Most people who open a shortcut editor do not know which combinations are free. A little guidance prevents the round trip of choosing a key, discovering it does nothing because a web app they use swallows it, and giving up.

Three things are worth showing next to the editor:

  • Which modifiers are reliable. Alt+Shift+ and Ctrl+Shift+ combinations collide least with browser and site shortcuts.
  • Which keys are already bound by your extension. Obvious, but frequently missed — a user may pick the same key for two of your commands.
  • What happens on conflict. On Chrome, a key already used by the browser or another extension is refused on the shortcuts page itself; on Firefox, commands.update rejects. Either way, the key they wanted may not be available.
1function conflictsWithOwn(shortcut, commands, exceptName) {
2  return commands.some((c) => c.name !== exceptName && c.shortcut && c.shortcut === shortcut);
3}

Execution context: the options page. Checking your own commands is the one conflict the extension can detect locally; conflicts with the browser and other extensions are only discovered when the browser refuses. The broader problem is covered in resolving keyboard shortcut conflicts.

How often a user-chosen combination is refused or shadowedShare of user-chosen shortcuts that end up refused by the browser or shadowed by a common web application, by modifier combination.Ctrl + letter71 % problema…browser and sitesCtrl + Shift + letter28 % problema…Alt + Shift + letter9 % problemat…Ctrl + Alt + letter14 % problema…AltGr on some layouts
Two-modifier combinations with Alt are the least contested — worth recommending in the editor.

Keyboard layouts and what “the same key” means

A shortcut is stored as a key name, and key names do not mean the same physical key on every layout. Alt+Shift+Z on a German keyboard is the key where English users have Y; Alt+Shift+Q on a French AZERTY keyboard is in the A position. Suggested keys chosen on a US keyboard can therefore land somewhere awkward — or collide with a layout-specific character input — for a large share of users.

Two layouts need particular care. On many European layouts, Ctrl+Alt is equivalent to AltGr, which types characters such as @, { and . A shortcut on Ctrl+Alt+<letter> can make it impossible to type that character anywhere in the browser while your extension is installed — a bug that surfaces as “I can’t type an @ sign in Gmail” and is very hard to trace back to an extension. Avoid Ctrl+Alt combinations for suggested keys entirely.

The second is non-Latin layouts, where the letter keys produce Cyrillic, Greek or other characters. Browsers match shortcuts by physical key position in most of these cases, so a binding on Alt+Shift+S still works — but a hint telling a Russian-layout user to press “S” is confusing when the key is labelled “Ы”. Where possible, show the binding exactly as commands.getAll() returns it and let the user map it, rather than rendering your own description of the key.

Rebinding is the escape hatch for all of this, which is the strongest argument for making it easy to find: the users most affected by a poor default are exactly the ones who cannot fix it without knowing the shortcuts page exists.

Cross-browser variation

  • Chrome / Edge: users edit bindings at chrome://extensions/shortcuts (edge://extensions/shortcuts), which can scope a command to the browser or make it global. Extensions can read but never write bindings.
  • Firefox: browser.commands.update, commands.reset and commands.onChanged enable a full in-extension editor. Users can also edit from the add-ons manager’s “Manage extension shortcuts”.
  • Safari: bindings are read-only for the extension and editing is less discoverable for users. The in-UI hint is the main thing you can offer.
  • All three: Ctrl in a binding maps to Command on macOS unless the manifest used MacCtrl; display the platform’s own symbols rather than the manifest string where you can.

Verification

  1. Change a binding on the browser’s shortcuts page, return to your options tab and confirm the list updates without a reload.
  2. On Firefox, bind a command through your editor and confirm with:
1(await browser.commands.getAll()).find((c) => c.name === "save-page").shortcut;
2// "Alt+Shift+K"

Execution context: the options page console. Then try binding a plain letter and confirm your UI shows the rejection message.

  1. Remove the binding for save-page and confirm the popup button’s tooltip no longer mentions a key.
  2. Assign the same key to two of your own commands and confirm the editor warns.

FAQ

Why can’t a Chrome extension change its own shortcuts?

Deliberately, so that an extension cannot silently take over keys the user relies on. The shortcuts page is the user’s control point.

No. chrome://extensions/shortcuts opens the full list; the user finds your extension’s section there.

Should I store the user’s preferred shortcuts myself?

No — the browser is the source of truth, and storing a copy means it can disagree. Read with getAll() whenever you need to display a binding.

Other UI/UX Patterns & Interactive Components Resources