browser-extension-architecture

Guides MV3 browser extension architecture decisions for Chromium and Firefox builds from one source.

4|Updated Jun 21, 2026
One-click install
npx skills add https://github.com/NEETROF/cymbra --skill browser-extension-architecture-neetrof
Or copy as Structured Prompt for Agent▼
Please help me install this Agent Skill.
Skill: browser-extension-architecture
Source: https://github.com/NEETROF/cymbra/tree/main/.claude/skills/browser-extension-architecture
Command: npx skills add https://github.com/NEETROF/cymbra --skill browser-extension-architecture-neetrof

SYSTEM DOCUMENTATION & REQUIREMENTS

What problem does it solve? Building a Manifest V3 extension that ships to both Chromium and Firefox from one codebase involves platform constraints that are expensive to rediscover by trial: who can open the side panel versus the sidebar, how content-script injection differs on Firefox for Android, and how to keep one view implementation across multiple hosts. This Skill encodes those hard-won platform truths and the framing questions to settle before writing any extension UI code. ## Core Features & Use Cases - Surface decision framing: Five scoping questions to answer before building any UI surface (HUD, popup, side panel, drawer), enforcing consistency across entry points and a stay-in-the-reading-page principle. - Platform truth table: Documents exact gesture and messaging rules for chrome.sidePanel.open versus sidebarAction.open, activeTab-first injection on Chromium, and static content_scripts on Firefox including GeckoView/Android limitations. - One-impl-N-hosts pattern: Reuses single view builders (mountReview, mountStats, mountSettings) across the side panel and in-page drawer, with closed shadow DOM, design tokens, and double-init guards. - Use Case: Before adding a stats view that opens from both the in-page HUD and the toolbar popup, consult this Skill to learn that Firefox requires the in-page drawer while Chromium can use the Side Panel, then reuse the existing mountStats builder in both hosts. ## Quick Start Ask the assistant to apply the browser-extension-architecture rules before building a new popup, side panel, or in-page drawer surface in the extension.

Frequently Asked Questions about browser-extension-architecture

High-intent search queries and answers about installing and using this skill.

FAQPage Schema
How do I open the Chrome side panel from a content script click?▼

The background must call chrome.sidePanel.open synchronously inside runtime.onMessage using sender.tab.id, with no await before the call. Chrome propagates the content-script click's user activation across that one message; any async step spends the gesture and the call fails.

How do I build one extension for both Chromium and Firefox?▼

Use a single source with a build-time target constant such as __TARGET__ set to chromium or firefox, producing separate dist bundles. Expect per-browser surface divergence, but keep destinations consistent within each browser and prefer staying in the reading page.

Can a Firefox sidebar be opened from an in-page element?▼

No. sidebarAction.open requires a user gesture that does not survive the content-script to background message, so a page element like a HUD cannot open it. Use an in-page drawer from the HUD on Firefox, or a keyboard commands.onCommand handler which keeps its gesture.

Why does dynamic content script registration fail on Firefox for Android?▼

MV3 dynamic registration does not reliably fire on GeckoView, and browser.contentScripts.register is torn down when the non-persistent event page unloads after about 30 seconds. Ship a static content_scripts entry on all URLs so injection runs on every load and reload.

Why does the hidden attribute not hide my flex element?▼

The hidden attribute loses to display:flex or display:grid because they have equal specificity and the author rule wins. Add an explicit rule like .x[hidden]{display:none} for any toggled-hidden flex or grid container.

How do I avoid duplicating a view across the side panel and an in-page drawer?▼

Keep one builder per view, such as mountReview or mountSettings, that renders into a passed container and returns a refresh function. Share the CSS across hosts and treat any content difference between hosts as a bug rather than copying markup into a second file.