Resolving Keyboard Shortcut Conflicts in Chrome Extensions

Detect and resolve keyboard shortcut conflicts in MV3 extensions using chrome.commands.getAll(), badge warnings, and the browser shortcuts management page.

Published June 19, 2026 Updated June 19, 2026 8 min read
Table of Contents

The symptom is straightforward: your extension’s keyboard shortcut simply does nothing. No error appears. The user presses the combo and the command never fires. This guide is part of the Keyboard Shortcuts & Commands reference for MV3 extensions.

The conflict is usually invisible at install time. Chrome silently drops or ignores a suggested_key that clashes with a reserved browser combo, an OS-level binding, or a shortcut already claimed by another installed extension. Your manifest loads without error, the extension installs normally, and the shortcut entry disappears into a void the developer never sees.

Root cause: the browser resolves conflicts outside your extension’s control

MV3 registers shortcuts declaratively in manifest.json before any JavaScript runs. The browser’s shortcut manager evaluates the requested combo at install time against three reservation layers: OS-level bindings (Alt+Tab, Cmd+Space, Win+key), browser-reserved combos (Ctrl+W, Ctrl+T, Ctrl+N, Ctrl+Shift+I, Ctrl+Shift+J, F12), and combos already claimed by other installed extensions. If any layer claims the combo, Chrome silently unsets your shortcut. The chrome.commands API has no update() method — your extension cannot programmatically assign or reassign shortcuts. Only the user can do that, through the browser’s shortcut management page.

Step-by-step solution

Step 1: Detect the conflict on service worker startup

Call chrome.commands.getAll() when the service worker initialises. When a shortcut has been unset or overridden by a conflict, the shortcut field on the returned command object is an empty string "". A non-empty string means the combo is active.

 1// background.js  (MV3 service worker)
 2chrome.runtime.onInstalled.addListener(async () => {
 3  const commands = await chrome.commands.getAll();
 4
 5  for (const command of commands) {
 6    if (command.name === 'toggle-feature' && command.shortcut === '') {
 7      console.warn(
 8        '[shortcuts] toggle-feature has no active shortcut — likely a conflict.'
 9      );
10      // Persist the conflict state so popup/options page can display a warning
11      await chrome.storage.local.set({ shortcutConflict: true });
12    }
13  }
14});

Execution context: background.js — MV3 service worker, runs on onInstalled. chrome.commands.getAll() returns the runtime state of every registered command, including those whose suggested_key was silently dropped. The shortcut field reflects what is actually bound, not what manifest.json requested. This check also runs correctly after a service worker restart because storage persists the flag across lifetimes.

Step 2: Show an in-extension warning when a conflict is detected

Reading shortcutConflict from storage in the popup or options page lets you surface a targeted warning without requiring the user to open DevTools. A badge on the extension icon is the fastest signal.

 1// background.js  (MV3 service worker — add alongside the onInstalled listener)
 2async function updateConflictBadge() {
 3  const { shortcutConflict } = await chrome.storage.local.get('shortcutConflict');
 4
 5  if (shortcutConflict) {
 6    await chrome.action.setBadgeText({ text: '!' });
 7    await chrome.action.setBadgeBackgroundColor({ color: '#E53E3E' });
 8    await chrome.action.setTitle({
 9      title: 'Keyboard shortcut conflict — click to fix',
10    });
11  } else {
12    await chrome.action.setBadgeText({ text: '' });
13    await chrome.action.setTitle({ title: 'My Extension' });
14  }
15}
16
17chrome.runtime.onInstalled.addListener(async () => {
18  // ... conflict detection from Step 1 ...
19  await updateConflictBadge();
20});
21
22chrome.runtime.onStartup.addListener(updateConflictBadge);

Execution context: background.js — MV3 service worker. chrome.action.setBadgeText and setBadgeBackgroundColor work without the "tabs" permission. Hooking both onInstalled and onStartup covers installs, updates, and browser restarts. The badge persists across service-worker hibernations because it is stored in browser state, not in worker memory.

Step 3: Guide the user to the shortcut rebind page

Chrome provides no API to reassign shortcuts on behalf of the user. The only path is directing them to chrome://extensions/shortcuts. Open it programmatically from a popup or options page button.

1// popup.js  (runs in the popup renderer, NOT the service worker)
2document.getElementById('btn-fix-shortcut').addEventListener('click', () => {
3  chrome.tabs.create({ url: 'chrome://extensions/shortcuts' });
4  window.close(); // close the popup so the new tab is visible
5});

Execution context: Popup renderer process. chrome.tabs.create() requires the "tabs" permission or, alternatively, is available without it in popup and options page contexts because they are trusted extension pages. The chrome://extensions/shortcuts URL works in Chrome and Edge. For Firefox, the equivalent is about:addons — the user navigates to the Add-ons Manager and chooses “Manage Extension Shortcuts.” Never attempt to navigate to chrome:// URLs from a content script; that call is silently ignored.

Step 4: Verify the rebind with a second getAll() call

After the user reassigns the shortcut, your extension has no push notification. Poll getAll() when the popup or options page gains focus, then clear the conflict flag if the shortcut is now bound.

 1// popup.js
 2async function checkAndClearConflict() {
 3  const commands = await chrome.commands.getAll();
 4  const cmd = commands.find((c) => c.name === 'toggle-feature');
 5
 6  if (cmd && cmd.shortcut !== '') {
 7    // Shortcut is now active — clear the warning state
 8    await chrome.storage.local.set({ shortcutConflict: false });
 9    await chrome.action.setBadgeText({ text: '' });
10    document.getElementById('conflict-banner').hidden = true;
11  }
12}
13
14// Run the check each time the popup opens
15checkAndClearConflict();

Execution context: Popup renderer. This runs synchronously when the popup HTML loads, so the UI reflects the current shortcut state before the user sees it. There is no chrome.commands.onChanged event — polling on popup open is the only available mechanism in MV3.

Cross-browser variation

  • Chrome: chrome.commands.getAll() returns an empty shortcut string for any command that was silently unset due to a conflict. The shortcut management UI is at chrome://extensions/shortcuts. Up to four shortcuts per extension are permitted.
  • Edge: Behaviour mirrors Chrome. The shortcut management UI is also reachable at edge://extensions/shortcuts. Edge ships its own set of reserved combos that partially differ from Chrome’s — test on both if you ship to Edge.
  • Firefox: browser.commands.getAll() returns the same empty-string signal for unset shortcuts. Firefox’s limit is also four shortcuts per extension. Users manage shortcuts in the Add-ons Manager at about:addons under “Manage Extension Shortcuts.” Firefox does not reserve Ctrl+W or Ctrl+T at the extensions level the same way Chrome does, so some combos blocked in Chrome work in Firefox.
  • Safari: Keyboard shortcut support in Safari extensions is extremely limited. Safari does not honour the commands manifest key in the same way. Users enable shortcuts manually in System Settings under Extensions, and available modifiers are constrained to what macOS allows. Do not rely on getAll() returning useful data in Safari — treat shortcut availability as undefined there.
  • macOS (all browsers): The Command modifier maps to Cmd. Ctrl on macOS maps to the Control key, which is rarely used in Mac shortcuts. Use MacCtrl in suggested_key when you specifically need the macOS Control key. Cmd+Space (Spotlight), Cmd+Tab (app switcher), and Cmd+Q are OS-reserved and cannot be captured. For Mac-primary extensions, always declare a mac key in suggested_key alongside default.
  • Windows/Linux: Win+key combinations are OS-reserved on Windows. Linux generally mirrors Windows for browser shortcut purposes. Both platforms use Ctrl as the primary modifier.

Verification

  1. Open chrome://extensions, find your extension, and click “Service worker” to open the background DevTools panel.
  2. In the console, run: chrome.commands.getAll().then(cmds => console.table(cmds.map(c => ({ name: c.name, shortcut: c.shortcut, desc: c.description })))).
  3. Confirm that the shortcut column for your command contains the expected combo (e.g., Ctrl+Shift+Y), not an empty string.
  4. If it is empty, navigate to chrome://extensions/shortcuts, assign a combo that is not reserved, reload the extension, and repeat steps 1–3.
  5. After rebinding, verify shortcutConflict in storage is cleared: chrome.storage.local.get('shortcutConflict').then(console.log) — should log { shortcutConflict: false }.
  6. Press the newly bound combo in a normal browser tab and confirm your chrome.commands.onCommand listener fires — check for the expected log line in the service-worker console.

FAQ

Why does my shortcut appear correctly in manifest.json but chrome.commands.getAll() returns an empty string?

Chrome evaluates suggested_key at install time and silently unsets any combo that conflicts with a reserved or already-claimed shortcut. The manifest is not updated — it still shows your requested combo. getAll() returns the runtime state, which is the truth. The two values can permanently diverge without any error being thrown.

Can I detect which other extension is claiming my combo?

No. Chrome does not expose which extension owns a conflicting shortcut. The only diagnostic available to the user is the chrome://extensions/shortcuts page, which lists all installed extensions and their active shortcuts side by side. There is no API for your extension to introspect other extensions’ shortcut registrations.

Is there any way to programmatically set a shortcut without user interaction?

No. chrome.commands in MV3 has no update() or set() method. The Chrome Web Store guidelines explicitly prohibit extensions from navigating users to chrome://extensions/shortcuts on first install without a user gesture, so always tie that navigation to an explicit UI action such as a button click. The user must make the change manually.

What happens to my shortcut when the extension is updated?

If the update changes the suggested_key in manifest.json, Chrome re-evaluates the new combo at update time. If the user had previously overridden the shortcut at chrome://extensions/shortcuts, their custom binding is preserved — the suggested_key in the updated manifest does not override a user’s manual assignment.

Other UI/UX Patterns & Interactive Components Resources