UI/UX Patterns & Interactive Components

Build production-ready extension UIs in MV3: popup design, options pages, keyboard shortcuts, context menus, and side panels — with accessibility, theming, and CSP patterns.

Manifest V3 eliminated persistent background pages, and that single change rewrites the rules for every UI surface an extension exposes. Popups can no longer assume a warm background process; options pages must hydrate their own state from storage on every open; keyboard shortcut handlers execute inside a service worker that may have been evicted seconds earlier. Getting these surfaces right requires understanding which execution context owns each piece of chrome API surface and how state flows between them without a long-lived coordinator. The popup interface design guide is the best first stop because the popup is both the most visible UI surface and the most constrained one.

MV3 extension UI surfaces and their execution contextsFive UI surfaces — popup, options page, commands, context menu, side panel / DevTools — mapped to the execution contexts that own them and the shared service worker they communicate through.Service workerbackground · event-drivenPopupaction.default_popupOptions pageoptions_ui / options_pageCommandscommands · onCommandContext menuscontextMenus APISide panel / DevToolschrome.storageshared state layer

Manifest declaration surface

All UI surfaces are declared once in manifest.json. The browser reads these at install time to register entry points; none of them are configurable at runtime. Getting the declaration wrong — using options_page instead of options_ui, or omitting the side_panel key — results in the surface simply not appearing, with no runtime error.

 1{
 2  "manifest_version": 3,
 3  "name": "My Extension",
 4  "version": "1.0",
 5  "action": {
 6    "default_popup": "popup.html",     // toolbar button → popup window
 7    "default_icon": "icon48.png",
 8    "default_title": "Open panel"
 9  },
10  "options_ui": {
11    "page": "options.html",            // opens in a tab within chrome://extensions
12    "open_in_tab": true                // false = embedded sheet in Chrome 111+
13  },
14  "commands": {
15    "_execute_action": {               // reserved: opens the popup via keyboard
16      "suggested_key": { "default": "Alt+Shift+U" },
17      "description": "Open UI"
18    },
19    "toggle-feature": {
20      "suggested_key": { "default": "Alt+Shift+F", "mac": "MacCtrl+Shift+F" },
21      "description": "Toggle the main feature on/off"
22    }
23  },
24  "side_panel": {
25    "default_path": "sidepanel.html"  // Chrome 114+; persistent beside the page
26  },
27  "permissions": ["contextMenus", "sidePanel", "storage", "activeTab"],
28  "background": {
29    "service_worker": "sw.js",
30    "type": "module"
31  }
32}

Execution context: Parsed by the browser at install and update time. Chrome and Edge fully support all five keys. Firefox (≥ 109 for MV3) does not yet support side_panel; use progressive enhancement. Safari (≥ 17) supports action, options_ui, and commands; side_panel is not supported as of Safari 18. The commands key supports at most 4 suggested shortcuts per extension; Chrome ignores any extras.

The popup is an HTML page that opens in a constrained floating window when the user clicks the toolbar icon. Its lifetime is brutally short: it is created on click and destroyed when it loses focus. Every initialization must be synchronous-looking to the user — meaning you read state from chrome.storage immediately on DOMContentLoaded, before painting anything meaningful.

Choosing the right surface for a piece of extension UIPopup, options page, side panel, context menu and injected in-page UI compared on lifetime, how the user reaches them and how much space they get.SurfaceLifetimeOpened bySpacePopupUntil blurToolbar click800 × 600 maxOptions pageWhole tabExtensions menuFull pageSide panelAcross navigationAction or APINarrow columnContext menuOne interactionRight clickMenu items onlyInjected in-page UIPage lifetimeAutomaticWhole viewport
Lifetime is the deciding column: anything the user must keep visible while they browse cannot live in the popup.
 1// popup.ts — hydrate on open, persist on change
 2document.addEventListener("DOMContentLoaded", async () => {
 3  const { enabled, theme } = await chrome.storage.local.get(["enabled", "theme"]);
 4  applyTheme(theme ?? "system");
 5  renderToggle(enabled ?? false);
 6});
 7
 8document.getElementById("toggle")!.addEventListener("change", async (e) => {
 9  const enabled = (e.target as HTMLInputElement).checked;
10  await chrome.storage.local.set({ enabled });
11  // Notify the service worker so it can update alarms or rules
12  chrome.runtime.sendMessage({ type: "SET_ENABLED", payload: enabled });
13});
14
15function applyTheme(theme: string) {
16  document.documentElement.setAttribute("data-theme", theme);
17}

Execution context: Popup runs in its own renderer process, separate from both the service worker and any content scripts. chrome.storage, chrome.runtime.sendMessage, and chrome.tabs (with activeTab granted by the user’s click) are available. window.localStorage is accessible but scoped to the extension origin — do not use it as a cross-context state store. Firefox and Safari popup windows close on the same focus-loss trigger as Chrome; Safari additionally enforces a 600 px maximum popup width. For architectural depth, see popup interface design.

Options page

The options page is a full HTML document, persistent as long as its tab is open. It is the right place for configuration that the user sets once and rarely revisits — themes, feature flags, account links, data export. Because it outlives multiple service worker evictions during its session, it must subscribe to chrome.storage.onChanged rather than reading once on load.

 1// options.ts — live-sync settings form with storage
 2async function initOptions() {
 3  const prefs = await chrome.storage.local.get(null); // load everything
 4  populateForm(prefs);
 5}
 6
 7chrome.storage.onChanged.addListener((changes, area) => {
 8  if (area !== "local") return;
 9  for (const [key, { newValue }] of Object.entries(changes)) {
10    syncFieldToUI(key, newValue); // update only changed fields
11  }
12});
13
14document.getElementById("save-btn")!.addEventListener("click", async () => {
15  const formData = collectFormValues();
16  await chrome.storage.local.set(formData);
17  showSavedBanner();
18});
19
20initOptions();

Execution context: Options page runs in an extension-origin tab, giving it full access to chrome.storage, chrome.tabs, chrome.runtime, and most other extension APIs. It does not inherit any content-script restrictions. chrome.storage.onChanged fires in every open extension context simultaneously, so if both the popup and the options page are open at the same time, both listeners fire on each write — guard against double-processing. For tabbed layout patterns, see options page layouts.

Keyboard shortcuts and commands

The commands manifest key registers keyboard shortcuts that the browser intercepts before any web page sees the keydown event. The handler always fires in the service worker via chrome.commands.onCommand — never in the popup or options page. This means any DOM manipulation triggered by a shortcut must go through a chrome.tabs.sendMessage call to a content script, or must update storage and let the UI surface react via onChanged.

 1// sw.js — handle declared commands
 2chrome.commands.onCommand.addListener(async (command, tab) => {
 3  if (command === "toggle-feature") {
 4    const { enabled } = await chrome.storage.local.get("enabled");
 5    await chrome.storage.local.set({ enabled: !enabled });
 6    // Push update to any open popup via storage.onChanged, no direct DOM access
 7  }
 8  if (command === "_execute_action") {
 9    // browser opens the popup natively; no code needed here
10  }
11});

Execution context: chrome.commands.onCommand fires in the service worker background. The service worker may have been dormant — the browser wakes it to deliver the event. Chrome enforces a limit of 4 custom shortcuts per extension. Firefox fires the identical event under browser.commands.onCommand with native Promises. Safari supports commands since Safari 17 but does not guarantee that suggested keys will not conflict with system shortcuts — always test on macOS. For conflict resolution strategies, see keyboard shortcuts and commands.

Accessibility, theming & CSP

Accessibility

The constraints every extension UI surface inheritsExtension pages run under a strict content security policy, a fixed viewport, the user's colour scheme and the browser's own keyboard model.Strict CSPno inline handlers, no remote scriptexternal files onlyFixed viewportpopup capped at 800 × 600no window resize APIUser colour schemeprefers-color-schemeboth themes at AA contrastBrowser keyboard modelreserved shortcuts winfour command slots
These four constraints apply to every surface on this page — designing against them once saves reworking each surface later.

Extension pages are real HTML documents and must meet the same WCAG 2.1 AA bar as any web page. The patterns that most often fail in extensions: popup buttons with no accessible label, options form fields missing <label> associations, and dynamic content updated via innerHTML that screen readers never see because no aria-live region announces the change.

 1<!-- popup.html — accessible toggle with live region -->
 2<button
 3  id="toggle"
 4  role="switch"
 5  aria-checked="false"
 6  aria-label="Enable feature"
 7>
 8  <span class="thumb" aria-hidden="true"></span>
 9</button>
10<div aria-live="polite" aria-atomic="true" id="status-msg"></div>
1// After state change, announce it
2document.getElementById("status-msg")!.textContent = enabled
3  ? "Feature enabled"
4  : "Feature disabled";
5(document.getElementById("toggle") as HTMLElement).setAttribute(
6  "aria-checked",
7  String(enabled)
8);

Execution context: Extension page renderer. ARIA live regions work in Chrome, Firefox, and Edge. Safari VoiceOver reads aria-live="polite" correctly in extension pages since Safari 16. Do not rely on role="alert" for repeated announcements — VoiceOver may suppress identical strings; change the text on each update.

Theming

CSS custom properties set at :root are the most portable theming approach for extension UIs. Read the OS preference with prefers-color-scheme and complement it with a user-controlled setting stored in chrome.storage.local. Apply the setting via a data-theme attribute on <html> so a single CSS selector handles both.

 1/* popup.css */
 2:root {
 3  --bg: #ffffff;
 4  --fg: #111827;
 5  --accent: #2563eb;
 6}
 7[data-theme="dark"] {
 8  --bg: #1e1e2e;
 9  --fg: #cdd6f4;
10  --accent: #89b4fa;
11}
12@media (prefers-color-scheme: dark) {
13  :root:not([data-theme="light"]) {
14    --bg: #1e1e2e;
15    --fg: #cdd6f4;
16    --accent: #89b4fa;
17  }
18}

Execution context: Static CSS loaded by any extension HTML page. Works identically across Chrome, Firefox, and Safari. Avoid color-scheme: dark on the <html> element without testing — Safari on macOS applies it to system scrollbars and form controls, which can produce unexpected contrast in the popup’s constrained viewport.

Content Security Policy

MV3 enforces a strict CSP on all extension pages by default: no inline <script> blocks, no inline event handlers (onclick="..."), no eval, no new Function, and no remote script tags. You cannot relax script-src in MV3 — Chrome rejects any manifest that tries to add 'unsafe-eval'.

Practical consequences:

  • Move all scripts to external .js files loaded via <script src="...">.
  • Replace innerHTML assignments with DOM methods (createElement, appendChild, replaceChildren) or use a trusted-types-safe template engine.
  • Bundlers that emit eval for source maps (Webpack’s devtool: 'eval') must be set to 'source-map' or 'inline-source-map' for production and development builds targeting extension pages.
  • Styles can be inline or external; the CSP does not restrict CSS. style-src defaults to 'self' 'unsafe-inline' in Chrome’s extension CSP, so inline <style> blocks and style attributes are permitted.

Firefox and Safari enforce the same script-src restriction. Firefox additionally rejects 'wasm-unsafe-eval' in extension pages unless the wasm permission is declared.

Cross-browser compatibility matrix

SurfaceChrome / EdgeFirefox (≥ 109)Safari (≥ 17)
action.default_popupFull supportFull support (browser_action merged into action)Full support
options_ui.open_in_tabSupportedSupportedSupported; embedded sheet not available
commands (4 max)Full supportFull support (browser.commands)Supported; system shortcut conflicts possible
contextMenusFull supportbrowser.contextMenus; requires menus permissionSupported; no image/video context types
sidePanel + chrome.sidePanelChrome 114+ / Edge 114+Not supportedNot supported
devtools.panelsFull supportFull supportPartial; inspectedWindow.eval restricted
CSS custom properties in popupFull supportFull supportFull support
prefers-color-scheme in extension pagesFull supportFull supportFull support
eval / new Function in extension pagesBlocked by MV3 CSPBlockedBlocked

What this section covers

The guides here each go deep on one surface. Popup interface design covers the sub-100 ms initialization pattern, size constraints, overflow fixes, and responsive layout with Tailwind CSS. Options page layouts explains tabbed layout construction, form state synchronization with storage, and schema versioning for settings that outlive extension updates. Keyboard shortcuts and commands covers the full command lifecycle from manifest declaration to service-worker handler, global vs. in-page scope, and strategies for detecting and resolving shortcut conflicts. Context menus and right-click actions walks through dynamic menu generation, selection-aware items, and delegating click handling to content scripts. Side panel and DevTools interfaces addresses the Chrome-only sidePanel API, DevTools panel creation, and the port-based communication bridge between the inspected page and the extension background. Notifications, badges and the action API covers the toolbar surface every extension owns and the rules for using it without becoming noise. Internationalization and accessibility covers the two concerns that share one infrastructure in an extension — a translation and labelling pass over static markup, plus keyboard order, right-to-left layout and live announcements in surfaces with no browser chrome.

Internationalization & Accessibility

Localise and make accessible every MV3 extension surface: the i18n API, _locales bundles, CSP-safe DOM translation, keyboard navigation, RTL layouts and live announcements.

4 topics

  • • Announcing Dynamic Updates to Screen Readers
  • • Localising Extension UI with the i18n API
  • • Making Popups and Options Keyboard Navigable
  • + 1 more

Notifications, Badges & the Action API

Signal state from an MV3 service worker with chrome.action badges, icons and titles plus chrome.notifications — including per-tab state, click routing, and Chrome, Firefox and Safari differences.

2 topics

  • • Rich Notifications with Buttons and Images
  • • Updating the Toolbar Badge from the Service Worker

Side Panel & DevTools Interfaces

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.

2 topics

  • • Side Panel Support Across Browsers
  • • Building a Side Panel UI in MV3

Context Menus & Right-Click Actions

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.

2 topics

  • • Rebuilding Context Menus After Worker Restarts
  • • Dynamic Context Menu Generation Based on Page Content

Keyboard Shortcuts & Commands

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.

3 topics

  • • Handling Shortcuts in Content Scripts
  • • Resolving Keyboard Shortcut Conflicts in Chrome Extensions
  • • Implementing Global Keyboard Shortcuts Safely

Options Page Layouts

Design scalable options page UIs for MV3 extensions — sidebar nav, tabbed sections, responsive grids, form components, autosave UX, and dark mode support.

4 topics

  • • Embedding Options in the Popup
  • • Validating and Resetting Options Forms
  • • Building a Tabbed Options Page Layout
  • + 1 more

Popup Interface Design

Design MV3 extension popups that open fast, stay within the 800×600 px cap, load state from storage, respect CSP, and work across Chrome, Firefox, and Safari.

4 topics

  • • Dark Mode in Extension Popups
  • • Preserving Popup State When It Closes
  • • Fixing Popup Size and Overflow Issues
  • + 1 more