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.
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
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.
2. Link Chrome users to the browser’s editor
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.
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+andCtrl+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.updaterejects. 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.
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.resetandcommands.onChangedenable 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:
Ctrlin a binding maps toCommandon macOS unless the manifest usedMacCtrl; display the platform’s own symbols rather than the manifest string where you can.
Verification
- Change a binding on the browser’s shortcuts page, return to your options tab and confirm the list updates without a reload.
- 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.
- Remove the binding for
save-pageand confirm the popup button’s tooltip no longer mentions a key. - 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.
Can I deep-link to one command on the shortcuts page?
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.
Related
- The _execute_action command and the four-shortcut limit — which commands ship unbound.
- Resolving keyboard shortcut conflicts — what a refused binding means.
- Shortcut behaviour in Firefox and Safari — the engine differences in depth.
- Keyboard shortcuts and commands — the parent guide.