"In MV3, the toolbar surface is unified: one icon, one badge, one popup, one API. Lesson 1 walks the chrome.action toolbox — small, focused, and yours to repaint per tab whenever something interesting changes."
One Action to Rule Them All
MV2 had two competing concepts:
browser_action— always-visible toolbar icon. The default UX for most extensions.page_action— toolbar icon that only appeared when relevant for the current page (think bookmark stars).
MV3 collapsed both into chrome.action. Every extension that wants a toolbar presence declares an "action" object in the manifest, and Chrome shows the icon for every tab by default. To mimic the old page-action behavior (only show on certain pages), you call chrome.action.disable(tabId) / enable(tabId) from the SW at runtime.
The Manifest Declaration
The minimum:
action.default_icon— the icon shown in the toolbar. Either a single string path or an object keyed by size.action.default_title— tooltip shown on hover.action.default_popup— HTML file Chrome opens on click. Omit to let your SW handle clicks viachrome.action.onClicked.
Icons should be 16, 32, 48, and 128 pixel variants. Chrome picks the right one based on display density and DevTools settings. Provide them as a multi-key object so Chrome chooses correctly instead of scaling a single image.
The Runtime API
chrome.action exposes a small surface of mutators, all callable from the SW:
setIcon({ tabId?, path })— override the icon globally or per tab.setTitle({ tabId?, title })— change the tooltip.setBadgeText({ tabId?, text })— set the little colored badge text overlay. ~4 char max recommended.setBadgeBackgroundColor({ tabId?, color })— RGB array or hex string.setBadgeTextColor({ tabId?, color })— Chrome 115+.setPopup({ tabId?, popup })— change which HTML opens on click.disable(tabId?)/enable(tabId?)— grey out the icon to suggest "not actionable on this page."getUserSettings()— read whether the user has the icon pinned to the toolbar (vs hidden in the puzzle-piece menu).
Notice: every mutator takes an optional tabId. With tabId → that tab only. Without tabId → global default applied to every tab that has not been overridden.
The Click Behavior
Two mutually-exclusive paths for what happens when the user clicks the icon:
- Popup mode (default if
action.default_popupis set). Click opens the popup;chrome.action.onClickednever fires. - SW-handled mode (no
default_popup). Click fireschrome.action.onClickedin the SW; you decide what to do — open a side panel, run a script, send a message.
You can switch between them at runtime via chrome.action.setPopup({ tabId, popup: '' }) (empty popup string disables the popup for that tab and re-enables onClicked).
The Badge as a Signal
Badges are tiny — a max of ~4 characters at most browser scales — so they communicate one piece of information clearly:
- Count. Unread messages, clips saved today, tasks pending.
- Status. Two letters indicating mode ("ON" / "OFF", "REC" / "").
- Alert. A red exclamation when something needs attention.
Empty text (setBadgeText({ text: '' })) clears the badge. ClipDeck will use the badge for today's clip count (Track 5 Lesson 5) and for the per-tab paused state.
chrome.action.getUserSettings() returns { isOnToolbar: boolean }. If false, your icon is hidden behind the puzzle-piece menu — useful to detect on first-install and prompt the user to pin it. Without pinning, users forget you exist.