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.
Detect and resolve keyboard shortcut conflicts in MV3 extensions using chrome.commands.getAll(), badge warnings, and the browser shortcuts management page.
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.
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.
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.
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.
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.
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.
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://extensions/shortcuts. Edge ships its own set of reserved combos that partially differ from Chrome’s — test on both if you ship to Edge.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.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.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.Win+key combinations are OS-reserved on Windows. Linux generally mirrors Windows for browser shortcut purposes. Both platforms use Ctrl as the primary modifier.chrome://extensions, find your extension, and click “Service worker” to open the background DevTools panel.chrome.commands.getAll().then(cmds => console.table(cmds.map(c => ({ name: c.name, shortcut: c.shortcut, desc: c.description })))).shortcut column for your command contains the expected combo (e.g., Ctrl+Shift+Y), not an empty string.chrome://extensions/shortcuts, assign a combo that is not reserved, reload the extension, and repeat steps 1–3.shortcutConflict in storage is cleared: chrome.storage.local.get('shortcutConflict').then(console.log) — should log { shortcutConflict: false }.chrome.commands.onCommand listener fires — check for the expected log line in the service-worker console.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.
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.
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.
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.
chrome.storage.Implement persistent side panels and DevTools extension panels in MV3 using chrome.sidePanel and chrome.devtools.panels — per-tab control, lifecycle, and cross-browser gaps.
Build chrome.contextMenus items in MV3: register in onInstalled, handle onClicked in the service worker, create parent/child menus, and gate visibility by context type.
Register and handle keyboard shortcuts in MV3 extensions using the chrome.commands API — manifest declaration, service worker listeners, per-OS suggested keys, the 4-shortcut limit, and cross-browser rebinding via chrome://extensions/shortcuts.
Design scalable options page UIs for MV3 extensions — sidebar nav, tabbed sections, responsive grids, form components, autosave UX, and dark mode support.