"The keyboard shortcut is for power users. The popup is for discovery. The right-click menu is for everyone in between — the user who selected some text and isn't quite sure how to save it. Lesson 3 adds the menu that makes ClipDeck obvious without making it loud."
The API Shape
chrome.contextMenus is small and entirely SW-side. Three calls cover almost everything:
create({ id, title, contexts, parentId? })— add a menu item. Returns the id (useful when you let Chrome auto-generate it).update(id, { title?, enabled?, visible? })— change an existing item without recreating it.remove(id)— delete an item.removeAll()— wipe everything (useful at install / startup before re-creating).
Plus the click event: chrome.contextMenus.onClicked.addListener((info, tab) => ...). info describes the click (which menu id, current selection text, page URL); tab is the tab where the click happened.
Contexts — Where the Menu Appears
The contexts array decides when the item is in the menu:
page— right-click on any page background (most permissive).selection— only when the user right-clicks on highlighted text. Perfect for "Save selection to ClipDeck."link— only on anchor tags.image— only on<img>elements.video,audio— on those media elements.editable— only on input fields / textareas.frame— inside iframes specifically.action— when the user right-clicks on the extension's toolbar icon (Chrome 88+).
An item with contexts: ['selection'] stays hidden until the user actually selects something. No menu noise on pages where ClipDeck cannot do anything useful.
The Install-Time Pattern
Context menus are stored by Chrome but you have to re-create them every install / startup. The idiomatic pattern:
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'clipdeck-save-selection',
title: 'Save "%s" to ClipDeck',
contexts: ['selection'],
});
});
});
The %s in the title is replaced by Chrome with the actual selected text (truncated). The user sees "Save 'service workers idle-evict' to ClipDeck" instead of a generic label — instantly readable.
Handling the Click
The onClicked handler runs in the SW. It receives an info object with the relevant fields populated:
info.menuItemId— which item was clicked. Always check this first; one listener can handle multiple items.info.selectionText— the selected text (forselectioncontext). Up to ~1024 chars.info.pageUrl— the page where the click happened.info.linkUrl— forlinkcontext, the destination URL.info.srcUrl— forimage/video/audio, the media URL.info.frameUrl— when the click was inside an iframe.
For ClipDeck's save flow, the SW can build a clip directly from info.selectionText, info.pageUrl, and tab.title — no message to the content script needed, because the context menu already gave you everything.
Nested Menus and Parent IDs
To group items under a submenu, create a parent item first (with no onclick, just a title) and pass its id as parentId on the children:
- Parent:
{ id: 'clipdeck-root', title: 'ClipDeck', contexts: ['selection'] } - Child A:
{ id: 'cd-save', parentId: 'clipdeck-root', title: 'Save selection', contexts: ['selection'] } - Child B:
{ id: 'cd-save-tagged', parentId: 'clipdeck-root', title: 'Save with tag…', contexts: ['selection'] }
Submenus help when you have more than 2–3 related actions. With just one, a flat top-level item is clearer.
contexts array keeps menus quiet where they aren't useful. %s in the title shows the user what they're about to save.