Showing Shortcut Hints in Extension UI
Teach users an extension's keyboard shortcuts without nagging: hints in buttons, tooltips and the action title, accessible kbd markup and aria-keyshortcuts, hiding hints for unassigned commands, and first-use discovery.
Table of Contents
- Where hints belong
- Step-by-step: accurate, accessible hints
- 1. Load bindings into a lookup
- 2. Render hints with kbd and aria-keyshortcuts
- 3. Put the popup shortcut in the action title
- 4. Style hints to be present but quiet
- 5. Nudge once after repeated mouse use
- 6. Prompt for unassigned shortcuts instead of hiding them forever
- 7. Show in-page shortcuts separately
- 8. Localise the modifier names
- 9. Offer a shortcut sheet
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
Keyboard shortcuts are the fastest way to use an extension, and most users never learn them. The ones who do usually learned from a hint shown at the right moment: a small Alt+Shift+S next to the Save button, a tooltip on the toolbar icon, a one-time “Next time, press…” after the third mouse click. Hints that are wrong — because the user rebound the shortcut or it was never assigned — teach the wrong thing and erode trust. This guide shows where to put hints, how to render them accessibly, how to keep them accurate from commands.getAll, and how to nudge without nagging. It belongs to keyboard shortcuts and commands.
Where hints belong
Hints work best where the user is already doing the thing the shortcut does. In the popup or side panel, put the shortcut inline next to the button it duplicates, in a muted style. On the toolbar icon, include it in the action title — the browser shows it as the tooltip on hover. In menus and command palettes, align shortcuts to the right as native menus do. In onboarding, show the one or two most valuable shortcuts, not all of them. Each hint must come from the current configuration, which chrome.commands.getAll returns, and must disappear when the command is unassigned rather than show a stale suggestion.
Step-by-step: accurate, accessible hints
1. Load bindings into a lookup
1// hints.js
2let bindings = new Map();
3export async function loadBindings() {
4 bindings = new Map((await chrome.commands.getAll()).filter((c) => c.shortcut).map((c) => [c.name, c.shortcut]));
5 return bindings;
6}
7export const shortcutFor = (name) => bindings.get(name) ?? null;
Execution context: extension pages and the service worker. Only assigned commands enter the map, so shortcutFor returns null for anything the user cannot actually press. Call loadBindings() when each page opens; popups are short-lived, so this is effectively always fresh. See listing configured shortcuts with commands.getAll.
2. Render hints with kbd and aria-keyshortcuts
1export function decorateButton(button, commandName) {
2 const sc = shortcutFor(commandName);
3 button.querySelector(".hint")?.remove();
4 button.removeAttribute("aria-keyshortcuts");
5 if (!sc) return;
6 const hint = document.createElement("span");
7 hint.className = "hint";
8 hint.setAttribute("aria-hidden", "true");
9 for (const part of splitShortcut(sc)) hint.append(Object.assign(document.createElement("kbd"), { textContent: part }));
10 button.append(hint);
11 button.setAttribute("aria-keyshortcuts", toAriaKeys(sc)); // "Control+Shift+S"
12}
Execution context: the popup or side panel. The visible hint is aria-hidden to avoid screen readers reading “Save Ctrl Shift S” as part of the button name; instead, aria-keyshortcuts exposes the shortcut in a structured way that assistive technology can announce as a hint. Its value uses full key names (Control, Shift, Alt, Meta) joined with +.
3. Put the popup shortcut in the action title
1// sw.js
2async function refreshActionTitle() {
3 const [open] = (await chrome.commands.getAll()).filter((c) => c.name === "_execute_action");
4 const base = chrome.i18n.getMessage("extName");
5 await chrome.action.setTitle({ title: open?.shortcut ? `${base} (${open.shortcut})` : base });
6}
7chrome.runtime.onStartup.addListener(refreshActionTitle);
8chrome.runtime.onInstalled.addListener(refreshActionTitle);
Execution context: the service worker. The toolbar tooltip is the most-seen hint of all. Because there is no change event in Chrome, also refresh it when the popup opens (send a message from the popup) so a rebinding is reflected soon after. See setting the action title and tooltip.
4. Style hints to be present but quiet
1.hint { margin-inline-start: auto; display: inline-flex; gap: 2px; opacity: .7; font-size: .8em; }
2kbd { font: inherit; padding: 0 .35em; border: 1px solid currentColor; border-radius: 4px; line-height: 1.4; }
3@media (forced-colors: active) { kbd { border-color: ButtonText; } }
Execution context: extension page CSS. margin-inline-start: auto pushes the hint to the end of the button in both LTR and RTL layouts. Borders keep key chips visible in forced colours mode, as covered in respecting reduced motion and forced colors.
5. Nudge once after repeated mouse use
1// popup.js
2async function trackMouseUse(commandName) {
3 const sc = shortcutFor(commandName);
4 if (!sc) return;
5 const key = `mouseUses:${commandName}`;
6 const { [key]: n = 0, nudged = [] } = await chrome.storage.local.get([key, "nudged"]);
7 await chrome.storage.local.set({ [key]: n + 1 });
8 if (n + 1 >= 3 && !nudged.includes(commandName)) {
9 showToast(chrome.i18n.getMessage("shortcutNudge", [sc])); // "Tip: press $1 next time"
10 await chrome.storage.local.set({ nudged: [...nudged, commandName] });
11 }
12}
Execution context: the popup. A nudge after the third mouse use reaches users who repeat an action, at the moment they are doing it, and it never repeats. Counting locally keeps it private. Use a polite live region for the toast so screen reader users hear it without losing focus.
6. Prompt for unassigned shortcuts instead of hiding them forever
In settings, show unassigned commands with a “Set a shortcut” link to the browser’s shortcut page. Elsewhere, simply omit the hint. Users who want shortcuts will find them in settings; users who don’t are not bothered.
7. Show in-page shortcuts separately
Shortcuts handled by a content script (keydown listeners on the page) are not in commands.getAll. Keep them in your own registry so they can be listed alongside commands, and make them configurable to avoid conflicts with sites. See handling shortcuts in content scripts.
8. Localise the modifier names
On macOS Chrome returns ⇧⌘S; on Windows Ctrl+Shift+S. If you construct hints yourself (for in-page shortcuts), use the same conventions per platform, and localise key names where appropriate — German keyboards label Ctrl as “Strg”. See Mac and Windows modifier keys in commands.
9. Offer a shortcut sheet
1document.addEventListener("keydown", (e) => {
2 if (e.key === "?" && !e.target.closest("input, textarea, [contenteditable]")) {
3 document.querySelector("#shortcut-sheet").showModal();
4 }
5});
Execution context: the popup or side panel. A ? key that opens a modal sheet listing every assigned command, plus in-page shortcuts from your own registry, gives keyboard users one place to learn everything. Ignore the key while the user is typing in a field so it never steals a literal question mark. Render the sheet from the same lookup as the inline hints so all surfaces agree.
Common mistakes
- Static hint text. Wrong after rebinding or conflict.
- Hints read as part of button names. Use
aria-hiddenplusaria-keyshortcuts. - Repeating nudges. Once per command is enough.
- Showing hints for unassigned commands. The key does nothing.
- Mixing in-page and browser shortcuts silently. List them separately.
Cross-browser variation
- Chrome / Edge:
getAllreturns platform-formatted strings; no change event, so refresh on UI open. - Firefox: same API plus
commands.onChanged, which lets you refresh hints immediately. - Safari: test the strings returned; keep hints optional so missing data simply hides them.
Verification
- Rebind a shortcut and confirm the popup button hint and toolbar tooltip update.
- Clear a shortcut and confirm its hint disappears everywhere.
- With a screen reader, focus the Save button and confirm the name is “Save” with the shortcut announced as a hint.
- Click Save three times and confirm the nudge appears once only.
FAQ
Does aria-keyshortcuts make the shortcut work?
No. It only describes it; the command still comes from the manifest or your key handler.
Should hints show on touch devices?
Hide them when no keyboard is likely — for example, under (hover: none) and (pointer: coarse) in CSS.
Can I show all shortcuts with “?”
Yes — a ? key handler in the popup that opens a shortcut sheet is a common, discoverable pattern.
Should hints be translated?
The key names returned by getAll are already localised by the browser on some platforms. Translate the surrounding copy (“Tip: press $1 next time”) through messages.json, and pass the shortcut as a placeholder so word order can change per language.
Related
- Listing configured shortcuts with commands.getAll — the data source.
- Building a command palette in an extension — hints in a palette.
- Toasts and inline feedback in popups — the nudge’s delivery.
- Keyboard shortcuts and commands — the parent topic.