How to Develop Chrome Extensions using Plasmo [Full Handbook] — Opportunihub
Course Remote

How to Develop Chrome Extensions using Plasmo [Full Handbook]

Preston Mayieka · Remote

At a glance

Type
Course
Organisation
Preston Mayieka
Location
Remote
Work mode
Remote
Deadline
Rolling / not stated
Posted
11 May 2026

About this course

<p>Chrome extensions are lightweight tools that enhance and personalize your browsing experience, whether that's managing passwords, translating pages, or adding entirely new features to websites you use every day.</p> <p>Millions of developers have published extensions to the Chrome Web Store, and building one is more approachable than you might think.</p> <p>In this handbook you'll go from zero to a published Chrome extension using TypeScript, React, and Plasmo, a modern framework that handles the repetitive setup and configuration so you can focus on writing features instead of boilerplate.</p> <p>Along the way you'll touch the real Chrome extension APIs that power production extensions: querying tabs, creating tab groups, and passing messages between different parts of an extension.</p> <p>By the end you'll have working code, a mental model of how extensions are structured, and everything you need to publish your own ideas to the Chrome Web Store.</p> <h2 id="heading-table-of-contents">Table of Contents</h2> <ul> <li><p><a href="#heading-what-is-plasmo">What is Plasmo?</a></p> </li> <li><p><a href="#heading-what-you-will-build">What You Will Build</a></p> </li> <li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p> </li> <li><p><a href="#heading-prerequisites">Prerequisites</a></p> </li> <li><p><a href="#heading-project-setup">Project Setup</a></p> </li> <li><p><a href="#heading-understanding-the-background-script">Understanding the Background Script</a></p> </li> <li><p><a href="#heading-building-the-popup-ui">Building the Popup UI</a></p> </li> <li><p><a href="#heading-testing-your-extension">Testing Your Extension</a></p> </li> <li><p><a href="#heading-next-steps-and-extension-ideas">Next Steps and Extension Ideas</a></p> </li> <li><p><a href="#heading-deploying-to-chrome-web-store">Deploying to Chrome Web Store</a></p> </li> </ul> <h2 id="heading-what-is-plasmo">What is Plasmo?</h2> <p><a href="https://www.plasmo.com/">Plasmo</a> is an open-source framework for building browser extensions. Think of it as the equivalent of Create React App or Next.js, but for Chrome extensions.</p> <p>Without Plasmo, building a Chrome extension requires manually writing a <code>manifest.json</code> file, wiring up build tooling, and configuring TypeScript and React yourself. Plasmo handles all of that.</p> <p>A single command scaffolds a working project with TypeScript and React already configured. It reads your <code>package.json</code> and generates the <code>manifest.json</code> Chrome requires, so you never edit it directly.</p> <p>Moreover, changes to your source files automatically rebuild and reload the extension in Chrome during development, and full type safety including types for Chrome's own APIs is available out of the box.</p> <p>Plasmo doesn't hide the Chrome extension concepts from you. You still use <code>chrome.tabs</code>, <code>chrome.runtime</code>, and the rest of the Chrome APIs directly. It just removes the tedious scaffolding so you can start building immediately.</p> <h2 id="heading-what-you-will-build">What You Will Build</h2> <p>In this tutorial, you'll build a <strong>Tab Grouper</strong> Chrome extension from scratch.</p> <p>This extension automatically organizes your browser tabs by grouping them based on their website domain.</p> <img src="https://cdn.hashnode.com/uploads/covers/64ef9ca6a3a26476fe998b69/43f51cde-41c8-46ac-9305-6b4ad5adc1ac.gif" alt="Animated demo of the Tab Grouper extension grouping open tabs into colored groups by domain" style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <h3 id="heading-example-use-case">Example Use Case</h3> <p>Imagine you have 20 tabs open: 5 from GitHub, 4 from YouTube, 3 from Stack Overflow, and 8 from other websites.</p> <p>With one click, the Tab Grouper extension will automatically create colored groups for each website, making it straightforward to find and manage your tabs.</p> <h2 id="heading-what-you-will-learn">What You Will Learn</h2> <p>By completing this tutorial, you'll get hands-on experience in three areas.</p> <p>First, <strong>Chrome Extension Basics</strong>: how extensions work under the hood, the anatomy of an extension (manifest, background scripts, popups), and how to load and test extensions in Chrome during development.</p> <p>Second, <strong>Chrome APIs</strong>: specifically <code>chrome.tabs</code> for managing browser tabs, <code>chrome.tabGroups</code> for creating and customizing tab groups, and <code>chrome.runtime</code> for passing messages between different parts of your extension.</p> <p>Third, <strong>Modern Web Development tooling</strong>: TypeScript for type-safe JavaScript, React for building the popup UI, and the Plasmo framework that ties it all together.</p> <h2 id="heading-prerequisites">Prerequisites</h2> <p>You don't need to be an expert in any of these, but you'll have the smoothest experience if you're comfortable with basic JavaScript or TypeScript and have a general understanding of HTML and CSS.</p> <p>Some familiarity with React is helpful but not required. The pop-up component we'll build is simple enough to follow even if you're new to it.</p> <p>On the software side, you'll need Node.js version 18 or higher (<a href="https://nodejs.org/">download here</a>), Google Chrome, a code editor (VS Code is recommended), and pnpm as your package manager.</p> <h3 id="heading-verify-your-setup">Verify Your Setup</h3> <p>Open your terminal and run these commands to confirm everything is installed:</p> <pre><code class="language-bash">node --version # Should output v18.0.0 or higher npm --version # Should output 9.0.0 or higher </code></pre> <h3 id="heading-getting-help">Getting Help</h3> <p>If you get stuck, review the complete code in the repository, consult the Chrome Extension documentation, or ask for help in the community forums.</p> <h3 id="heading-ready-to-begin">Ready to Begin?</h3> <p>In the next section, you'll set up your development environment and create your first Chrome extension project.</p> <p>Let's get started!</p> <h2 id="heading-project-setup">Project Setup</h2> <p>In this section, you'll use Plasmo to scaffold your Chrome extension project, then customize it for the Tab Grouper.</p> <p>Rather than creating files manually, you'll let Plasmo generate a starter project with all required configuration, then explore what was created before customizing it for our needs.</p> <h2 id="heading-step-1-install-pnpm-recommended">Step 1: Install pnpm (Recommended)</h2> <p>Plasmo officially recommends <strong>pnpm</strong> for faster installs and better disk space usage. Check if you already have it:</p> <pre><code class="language-bash">pnpm --version </code></pre> <p>If you see a version number, skip to Step 2.</p> <img src="https://cdn.hashnode.com/uploads/covers/64ef9ca6a3a26476fe998b69/aeed7b06-a403-4fe2-81fe-571a00219acf.png" alt="Terminal output showing pnpm version number after running pnpm --version" style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>If you get "command not found", install it with:</p> <pre><code class="language-bash">npm install -g pnpm </code></pre> <h2 id="heading-step-2-create-your-extension-project">Step 2: Create Your Extension Project</h2> <p>Run this command to create a new Plasmo project:</p> <pre><code class="language-bash">pnpm create plasmo tab-grouper </code></pre> <p>You'll see:</p> <pre><code class="language-plaintext">🟣 Creating a new Plasmo extension 📁 Project name: tab-grouper ? Extension description: (Give your extension a nice description) ? Author name: (Your Name) </code></pre> <p>Plasmo will then scaffold the project and install dependencies automatically. You might be prompted to enter a description and author name.</p> <p>Fill these in however you like.</p> <img src="https://cdn.hashnode.com/uploads/covers/64ef9ca6a3a26476fe998b69/e0a58818-0bec-42a7-bde3-c7a66de68b7a.png" alt="Terminal output showing Plasmo scaffolding a new project called tab-grouper and installing dependencies." style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <h3 id="heading-step-3-navigate-to-your-project">Step 3: Navigate to Your Project</h3> <pre><code class="language-bash">cd tab-grouper </code></pre> <h3 id="heading-step-4-explore-what-was-created">Step 4: Explore What Was Created</h3> <p>List the files that Plasmo generated:</p> <pre><code class="language-bash">ls -la </code></pre> <p>You should see something like this:</p> <pre><code class="language-plaintext">tab-grouper/ ├── .git/ # Git repository (already initialized!) ├── .github/ # GitHub Actions workflows ├── assets/ │ └── icon.png # Default Plasmo icon ├── node_modules/ # Dependencies (already installed!) ├── package.json # Project configuration ├── popup.tsx # Default popup ├── .prettierrc.cjs # Code formatting rules ├── .gitignore # Git ignore rules ├── README.md # Default readme └── tsconfig.json # TypeScript configuration </code></pre> <p>The key files to know about:</p> <ul> <li><p><strong>assets/icon.png</strong>: The extension icon required by Chrome.</p> </li> <li><p><strong>package.json</strong>: Lists dependencies and scripts, and is where you configure the extension manifest.</p> </li> <li><p><strong>popup.tsx</strong>: The UI that appears when you click the extension icon.</p> </li> <li><p><strong>tsconfig.json</strong>: Contains TypeScript settings that are already correctly configured.</p> </li> </ul> <h3 id="heading-step-5-test-the-default-extension">Step 5: Test the Default Extension</h3> <p>Make sure everything works <strong>before</strong> you customize it.</p> <p>You can do this by starting the development server:</p> <pre><code class="language-bash">pnpm dev </code></pre> <p>You should see output like this:</p> <pre><code class="language-plaintext">🟣 Plasmo v0.90.5 🔴 The Browser Extension Framework 🔵 INFO | Starting the extension development server... 🔵 INFO | Building for target: chrome-mv3 🔵 INFO | Loaded environment variables from: [] 🟢 DONE | Extension re-packaged in 1842ms! 🚀 View Extension: 📦 build/chrome-mv3-dev </code></pre> <p>Your extension is ready. Keep this terminal window open.</p> <p>Plasmo watches for file changes and rebuilds automatically.</p> <h3 id="heading-step-6-load-the-extension-in-chrome">Step 6: Load the Extension in Chrome</h3> <p>Now load the extension into Chrome to test it:</p> <ol> <li><p>Open Google Chrome</p> </li> <li><p>Go to <code>chrome://extensions/</code></p> </li> <li><p>Enable <strong>Developer mode</strong> (toggle in top-right)</p> </li> <li><p>Click <strong>"Load unpacked"</strong></p> </li> <li><p>Navigate to your project folder</p> </li> <li><p>Select the <code>build/chrome-mv3-dev</code> folder</p> </li> <li><p>Click "Select Folder"</p> </li> </ol> <img src="https://cdn.hashnode.com/uploads/covers/64ef9ca6a3a26476fe998b69/19cef596-a9d1-4709-8d27-594381d03842.gif" alt="Animated gif showing how to load an unpacked extension in Chrome via the Extensions page developer mode" style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>Your extension should now appear in the list.</p> <h3 id="heading-step-7-test-the-default-popup">Step 7: Test the Default Popup</h3> <ol> <li><p>Click the puzzle piece icon in Chrome's toolbar</p> </li> <li><p>Find "tab-grouper" and pin it</p> </li> <li><p>Click the extension icon</p> </li> </ol> <p>You will see a default popup that says "Welcome to Plasmo!"</p> <img src="https://cdn.hashnode.com/uploads/covers/64ef9ca6a3a26476fe998b69/56bad298-b07e-41c5-a648-49e382e0c51b.png" alt="The default Plasmo popup showing a Welcome to Plasmo message in the Chrome toolbar popup" style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>The extension is working. Now you can customize it.</p> <h3 id="heading-step-8-update-extension-information">Step 8: Update Extension Information</h3> <p>Open <code>package.json</code> in your editor. This file stores metadata about your project. name, version, description, dependencies, and scripts for building and running your extension.</p> <p>Find these lines near the top:</p> <pre><code class="language-json">{ "name": "tab-grouper", "displayName": "tab-grouper", "version": "0.0.0", "description": "A basic Plasmo extension.", </code></pre> <p>Change them to:</p> <pre><code class="language-json">{ "name": "tab-grouper", "displayName": "Tab Grouper", "version": "1.0.0", "description": "A simple Chrome extension - group tabs by domain", </code></pre> <p>Save the file.</p> <h3 id="heading-step-9-add-required-permissions-critical">Step 9: Add Required Permissions (Critical!)</h3> <p><strong>This is a critical step.</strong> Without permissions, your extension will fail with errors like:</p> <pre><code class="language-plaintext">TypeError: Cannot read properties of undefined (reading 'query') </code></pre> <p>Chrome extensions must declare which browser APIs they intend to use. In <code>package.json</code>, find the <code>"manifest"</code> section.</p> <p>It looks like this:</p> <pre><code class="language-json">"manifest": { "host_permissions": [ "https://*/*" ] } </code></pre> <p>Replace it with:</p> <pre><code class="language-json">"manifest": { "permissions": [ "tabs", "tabGroups" ] } </code></pre> <p>Save the file. The <code>tabs</code> permission allows you to read tab information (required for <code>chrome.tabs.query()</code>), and <code>tabGroups</code> allows you to create and manage tab groups (required for <code>chrome.tabGroups.update()</code>).</p> <h3 id="heading-finding-the-right-permissions-for-your-own-extensions">Finding the right permissions for your own extensions:</h3> <p>The <a href="https://developer.chrome.com/docs/extensions/reference/permissions-list">Chrome Extension Permissions Reference</a> lists every available permission and what it unlocks.</p> <p>Each API's documentation page also lists which permissions it requires, for example, the <a href="https://developer.chrome.com/docs/extensions/reference/api/tabs">chrome.tabs API page</a> specifies the <code>"tabs"</code> permission.</p> <p>If you're using Plasmo, the <a href="https://docs.plasmo.com/framework/customization/manifest">Manifest Configuration docs</a> explain how to add permissions through <code>package.json</code>.</p> <p>As a general rule: if you're getting <code>undefined</code> errors when calling a Chrome API, a missing permission is the first thing to check.</p> <h3 id="heading-step-10-verify-hot-reload-works">Step 10: Verify Hot Reload Works</h3> <p>Plasmo automatically reloads your extension when you save changes.</p> <p>Check the terminal where <code>pnpm dev</code> is running. After saving <code>package.json</code> you should see something like:</p> <pre><code class="language-plaintext">🔄 Reloading extension... ✅ Ready in 0.8s </code></pre> <p>Your project is now ready: a working extension loaded in Chrome, a development server running with hot reload, and the required permissions in place.</p> <p>Leave the dev server running and the extension loaded as you work through the next sections. Your changes will reload automatically.</p> <h3 id="heading-section-summary">Section Summary</h3> <p>In this section you installed pnpm, scaffolded a new extension with <code>pnpm create plasmo</code>, explored the generated project structure, started the development server, loaded the extension in Chrome, and updated the extension metadata and permissions.</p> <p><strong>Next:</strong> You'll create the background script that handles the tab grouping logic.</p> <h2 id="heading-understanding-the-background-script">Understanding the Background Script</h2> <p>The background script is the heart of your extension. It runs persistently behind the scenes and contains the core logic.</p> <p>In this case, the code that groups your tabs by domain.</p> <h3 id="heading-what-is-a-background-script">What is a Background Script?</h3> <p>A background script runs continuously even when the popup is closed.</p> <p>It can listen to browser events like tabs opening, closing, or updating, perform tasks that don't require direct user interaction, and communicate with other parts of the extension by passing messages.</p> <p>Think of it as the server-side of your extension. The popup is just a UI that talks to it.</p> <h3 id="heading-step-1-create-backgroundts">Step 1: Create background.ts</h3> <p>Plasmo's scaffolding didn't create a background script by default, so you'll create this file from scratch. Create a new file called <code>background.ts</code> in your project root (the same level as <code>popup.tsx</code>):</p> <pre><code class="language-typescript">export {} // Background script - runs in the background and handles tab grouping logic console.log("Tab Grouper background script loaded!") // Listen for messages from the popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) =&gt; { if (message.type === "GROUP_TABS") { groupTabsByDomain() sendResponse({ success: true }) } return true }) </code></pre> <p>The <code>export {}</code> at the top is required by Plasmo to treat this file as a module. Without it you may get errors about conflicting global variable declarations.</p> <p>The <code>console.log</code> will help you verify the script loaded correctly (you'll see it in the extension's DevTools console). <code>chrome.runtime.onMessage</code> sets up a listener so the background script can receive instructions from the popup.</p> <p>When it receives a <code>"GROUP_TABS"</code> message, it calls the grouping function.</p> <p>You can read more about this messaging pattern in the <a href="https://developer.chrome.com/docs/extensions/develop/concepts/messaging">Chrome Extensions documentation</a>.</p> <h3 id="heading-step-2-implement-tab-grouping-logic">Step 2: Implement Tab Grouping Logic</h3> <p>Now add the main grouping function below the message listener:</p> <pre><code class="language-typescript">async function groupTabsByDomain() { try { // Step 1: Get all tabs in the current window const tabs = await chrome.tabs.query({ currentWindow: true }) // Step 2: Create a Map to organize tabs by domain const domainGroups = new Map&lt;string, chrome.tabs.Tab[]&gt;() // Step 3: Loop through each tab and group by domain tabs.forEach(tab =&gt; { // Skip tabs without URLs if (!tab.url) return // Extract the domain from the URL const domain = getDomainFromUrl(tab.url) // Skip invalid domains (like chrome:// pages) if (!domain) return // Add tab to the appropriate domain group if (!domainGroups.has(domain)) { domainGroups.set(domain, []) } domainGroups.get(domain)!.push(tab) }) // Step 4: Create tab groups for each domain (only if 2+ tabs) for (const [domain, domainTabs] of domainGroups) { // Skip domains with only 1 tab if (domainTabs.length &lt; 2) continue // Get all tab IDs const tabIds = domainTabs .map(t =&gt; t.id!) .filter(id =&gt; id !== undefined) if (tabIds.length === 0) continue // Create the tab group const groupId = await chrome.tabs.group({ tabIds }) // Customize the group with a title and color await chrome.tabGroups.update(groupId, { title: domain, color: getColorForDomain(domain) // Randomized Tab Group colors. }) } console.log(`Successfully grouped ${domainGroups.size} domains`) } catch (error) { console.error("Error grouping tabs:", error) } } </code></pre> <p>The function starts by querying all tabs in the current window, then iterates over them to build a <code>Map</code> keyed by domain name.</p> <p>Once every tab has been sorted into a domain bucket, it loops through the map and calls <code>chrome.tabs.group()</code> for any domain that has two or more tabs, then immediately customizes the resulting group with a title and color.</p> <p>Domains with only a single tab are skipped. There's no point grouping a lone tab.</p> <h3 id="heading-step-3-extract-domain-helper">Step 3: Extract Domain Helper</h3> <p>Add a helper function to pull the hostname out of a URL:</p> <pre><code class="language-typescript">function getDomainFromUrl(url: string): string | null { try { const urlObj = new URL(url) // Skip Chrome internal pages (chrome://, chrome-extension://) if (urlObj.protocol === "chrome:" || urlObj.protocol === "chrome-extension:") { return null } // Remove "www." prefix and return the hostname return urlObj.hostname.replace(/^www\./, "") } catch { // Return null if URL is invalid return null } } </code></pre> <p><code>new URL(url)</code> gives us a structured object to work with rather than string-parsing the URL manually.</p> <p>The protocol check filters out Chrome's internal pages like <code>chrome://extensions</code> and <code>chrome://settings</code>, which extensions can't access.</p> <p>The <code>.replace(/^www\./, "")</code> ensures that <code>www.github.com</code> and <code>github.com</code> are treated as the same domain rather than two separate groups.</p> <p>The whole thing is wrapped in a try-catch so malformed URLs simply return <code>null</code> and get skipped.</p> <p>In practice: <code>https://www.github.com/user/repo</code> becomes <code>github.com</code>, <code>https://youtube.com/watch?v=123</code> becomes <code>youtube.com</code>, and <code>chrome://extensions</code> returns <code>null</code>.</p> <h3 id="heading-step-4-color-assignment-helper">Step 4: Color Assignment Helper</h3> <p>Add a function to deterministically assign a color to each domain:</p> <pre><code class="language-typescript">function getColorForDomain(domain: string): chrome.tabGroups.ColorEnum { // Available colors in Chrome const colors: chrome.tabGroups.ColorEnum[] = [ "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange" ] // Create a simple hash from the domain name let hash = 0 for (let i = 0; i &lt; domain.length; i++) { hash = domain.charCodeAt(i) + ((hash &lt;&lt; 5) - hash) } // Return a color based on the hash return colors[Math.abs(hash) % colors.length] } </code></pre> <p>Chrome supports eight colors for tab groups. Rather than assigning them randomly (which would change every time you group), this function hashes the domain name to a number and uses the modulo operator to pick a consistent index into the color array.</p> <p>The result is that <code>github.com</code> always gets the same color across sessions, while different domains are likely to get different colors.</p> <h3 id="heading-complete-backgroundts-file">Complete background.ts File</h3> <p>Your complete <code>background.ts</code> should look like this:</p> <pre><code class="language-typescript">export {} console.log("Tab Grouper background script loaded!") chrome.runtime.onMessage.addListener((message, sender, sendResponse) =&gt; { if (message.type === "GROUP_TABS") { groupTabsByDomain() sendResponse({ success: true }) } return true }) async function groupTabsByDomain() { try { const tabs = await chrome.tabs.query({ currentWindow: true }) const domainGroups = new Map&lt;string, chrome.tabs.Tab[]&gt;() tabs.forEach(tab =&gt; { if (!tab.url) return const domain = getDomainFromUrl(tab.url) if (!domain) return if (!domainGroups.has(domain)) { domainGroups.set(domain, []) } domainGroups.get(domain)!.push(tab) }) for (const [domain, domainTabs] of domainGroups) { if (domainTabs.length &lt; 2) continue const tabIds = domainTabs .map(t =&gt; t.id!) .filter(id =&gt; id !== undefined) if (tabIds.length === 0) continue const groupId = await chrome.tabs.group({ tabIds }) await chrome.tabGroups.update(groupId, { title: domain, color: getColorForDomain(domain) }) } console.log(`Successfully grouped ${domainGroups.size} domains`) } catch (error) { console.error("Error grouping tabs:", error) } } function getDomainFromUrl(url: string): string | null { try { const urlObj = new URL(url) if (urlObj.protocol === "chrome:" || urlObj.protocol === "chrome-extension:") { return null } return urlObj.hostname.replace(/^www\./, "") } catch { return null } } function getColorForDomain(domain: string): chrome.tabGroups.ColorEnum { const colors: chrome.tabGroups.ColorEnum[] = [ "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange" ] let hash = 0 for (let i = 0; i &lt; domain.length; i++) { hash = domain.charCodeAt(i) + ((hash &lt;&lt; 5) - hash) } return colors[Math.abs(hash) % colors.length] } </code></pre> <h3 id="heading-testing-the-background-script">Testing the Background Script</h3> <p>If your development server isn't already running from the previous section, start it:</p> <pre><code class="language-bash">pnpm dev </code></pre> <p>To verify the background script loaded correctly, go to <code>chrome://extensions</code>, find "Tab Grouper Tutorial", and click the <strong>"service worker"</strong> link.</p> <p>A DevTools console will open and you should see "Tab Grouper background script loaded!" confirming everything is wired up.</p> <h2 id="heading-building-the-popup-ui">Building the Popup UI</h2> <p>The popup is the small window that appears when a user clicks your extension icon in the Chrome toolbar.</p> <p>It can display information, provide buttons for actions, and show settings.</p> <p>In this section you'll build a React-based popup that shows live tab statistics and triggers the grouping logic in the background script.</p> <h3 id="heading-step-1-replace-popuptsx">Step 1: Replace popup.tsx</h3> <p>When you ran <code>pnpm create plasmo</code>, a default <code>popup.tsx</code> was created that just displays a welcome message.</p> <p>Open that file and replace <strong>all</strong> of its contents with this starting skeleton:</p> <pre><code class="language-tsx">import { useState, useEffect } from "react" function IndexPopup() { const [tabCount, setTabCount] = useState(0) const [groupCount, setGroupCount] = useState(0) const [isGrouping, setIsGrouping] = useState(false) return ( &lt;div&gt; &lt;h2&gt;Tab Grouper&lt;/h2&gt; &lt;button&gt;Group Tabs&lt;/button&gt; &lt;/div&gt; ) } export default IndexPopup </code></pre> <p>Save the file and the extension will automatically reload.</p> <p>The three state variables track the number of open tabs, the number of existing groups, and whether a grouping operation is currently in progress.</p> <p>That last one lets us disable the button and show a loading state so users can't trigger multiple groupings at once.</p> <h3 id="heading-step-2-load-statistics">Step 2: Load Statistics</h3> <p>Now add the logic to load tab and group counts when the popup opens. Add this inside the <code>IndexPopup</code> function, right after the state declarations:</p> <pre><code class="language-tsx">// Load tab statistics when popup opens useEffect(() =&gt; { loadStats() }, []) async function loadStats() { const tabs = await chrome.tabs.query({ currentWindow: true }) const groups = await chrome.tabGroups.query({ windowId: chrome.windows.WINDOW_ID_CURRENT }) setTabCount(tabs.length) setGroupCount(groups.length) } </code></pre> <p>The <code>useEffect</code> with an empty dependency array <code>[]</code> runs once when the component first mounts. In other words, every time the popup opens.</p> <p>It calls <code>loadStats</code>, which queries Chrome for the current window's tabs and groups, then updates the state variables with the counts.</p> <h3 id="heading-step-3-trigger-tab-grouping">Step 3: Trigger Tab Grouping</h3> <p>Add the handler that sends a message to the background script when the button is clicked:</p> <pre><code class="language-tsx">async function handleGroupTabs() { setIsGrouping(true) // Send message to background script await chrome.runtime.sendMessage({ type: "GROUP_TABS" }) // Refresh statistics await loadStats() setIsGrouping(false) } </code></pre> <p><code>chrome.runtime.sendMessage</code> delivers the <code>{ type: "GROUP_TABS" }</code> message to the listener we set up in <code>background.ts</code>.</p> <p>After the background script finishes, we reload the statistics so the group count updates immediately, then re-enable the button.</p> <h3 id="heading-step-4-build-the-ui">Step 4: Build the UI</h3> <p>Replace the placeholder <code>return</code> statement with this complete, styled version:</p> <pre><code class="language-tsx">return ( &lt;div style={{ width: 300, padding: 20, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }}&gt; {/* Header */} &lt;div style={{ marginBottom: 20 }}&gt; &lt;h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}&gt; 🗂️ Tab Grouper &lt;/h2&gt; &lt;p style={{ margin: "8px 0 0", fontSize: 13, color: "#666" }}&gt; Organize your tabs by domain &lt;/p&gt; &lt;/div&gt; {/* Statistics */} &lt;div style={{ display: "flex", gap: 12, marginBottom: 20, padding: 12, background: "#f5f5f5", borderRadius: 8 }}&gt; &lt;div style={{ flex: 1 }}&gt; &lt;div style={{ fontSize: 24, fontWeight: 600, color: "#333" }}&gt; {tabCount} &lt;/div&gt; &lt;div style={{ fontSize: 12, color: "#666" }}&gt; Open Tabs &lt;/div&gt; &lt;/div&gt; &lt;div style={{ flex: 1 }}&gt; &lt;div style={{ fontSize: 24, fontWeight: 600, color: "#0066ff" }}&gt; {groupCount} &lt;/div&gt; &lt;div style={{ fontSize: 12, color: "#666" }}&gt; Tab Groups &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; {/* Group Button */} &lt;button onClick={handleGroupTabs} disabled={isGrouping} style={{ width: "100%", padding: "12px 16px", fontSize: 14, fontWeight: 500, color: "white", background: isGrouping ? "#ccc" : "#0066ff", border: "none", borderRadius: 8, cursor: isGrouping ? "not-allowed" : "pointer", transition: "background 0.2s" }} &gt; {isGrouping ? "Grouping..." : "🗂️ Group Tabs by Domain"} &lt;/button&gt; {/* Footer */} &lt;div style={{ marginTop: 16, padding: 12, fontSize: 12, color: "#666", background: "#fff9e6", borderRadius: 6, border: "1px solid #ffe066" }}&gt; 💡 &lt;strong&gt;Tip:&lt;/strong&gt; This will group all tabs in this window by their website domain. &lt;/div&gt; &lt;/div&gt; ) </code></pre> <p>The UI has four parts: a header with the extension title and a short description, a statistics box showing the live tab and group counts side by side, the main action button (which grays out and changes text to "Grouping..." while work is in progress), and a tip box at the bottom.</p> <p>This tutorial uses inline styles for simplicity. In a production extension, you'd likely reach for CSS modules, Tailwind, or styled-components instead.</p> <h3 id="heading-complete-popuptsx-file">Complete popup.tsx File</h3> <p>Your complete <code>popup.tsx</code> should look like this:</p> <pre><code class="language-tsx">import { useState, useEffect } from "react" function IndexPopup() { const [tabCount, setTabCount] = useState(0) const [groupCount, setGroupCount] = useState(0) const [isGrouping, setIsGrouping] = useState(false) useEffect(() =&gt; { loadStats() }, []) async function loadStats() { const tabs = await chrome.tabs.query({ currentWindow: true }) const groups = await chrome.tabGroups.query({ windowId: chrome.windows.WINDOW_ID_CURRENT }) setTabCount(tabs.length) setGroupCount(groups.length) } async function handleGroupTabs() { setIsGrouping(true) await chrome.runtime.sendMessage({ type: "GROUP_TABS" }) await loadStats() setIsGrouping(false) } return ( &lt;div style={{ width: 300, padding: 20, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }}&gt; &lt;div style={{ marginBottom: 20 }}&gt; &lt;h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}&gt; 🗂️ Tab Grouper &lt;/h2&gt; &lt;p style={{ margin: "8px 0 0", fontSize: 13, color: "#666" }}&gt; Organize your tabs by domain &lt;/p&gt; &lt;/div&gt; &lt;div style={{ display: "flex", gap: 12, marginBottom: 20, padding: 12, background: "#f5f5f5", borderRadius: 8 }}&gt; &lt;div style={{ flex: 1 }}&gt; &lt;div style={{ fontSize: 24, fontWeight: 600, color: "#333" }}&gt; {tabCount} &lt;/div&gt; &lt;div style={{ fontSize: 12, color: "#666" }}&gt; Open Tabs &lt;/div&gt; &lt;/div&gt; &lt;div style={{ flex: 1 }}&gt; &lt;div style={{ fontSize: 24, fontWeight: 600, color: "#0066ff" }}&gt; {groupCount} &lt;/div&gt; &lt;div style={{ fontSize: 12, color: "#666" }}&gt; Tab Groups &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;button onClick={handleGroupTabs} disabled={isGrouping} style={{ width: "100%", padding: "12px 16px", fontSize: 14, fontWeight: 500, color: "white", background: isGrouping ? "#ccc" : "#0066ff", border: "none", borderRadius: 8, cursor: isGrouping ? "not-allowed" : "pointer", transition: "background 0.2s" }} &gt; {isGrouping ? "Grouping..." : "🗂️ Group Tabs by Domain"} &lt;/button&gt; &lt;div style={{ marginTop: 16, padding: 12, fontSize: 12, color: "#666", background: "#fff9e6", borderRadius: 6, border: "1px solid #ffe066" }}&gt; 💡 &lt;strong&gt;Tip:&lt;/strong&gt; This will group all tabs in this window by their website domain. &lt;/div&gt; &lt;/div&gt; ) } export default IndexPopup </code></pre> <h2 id="heading-testing-your-extension">Testing Your Extension</h2> <p>Now that you have both the background script and popup UI built, it's time to verify that everything works together in Chrome.</p> <h3 id="heading-step-1-make-sure-the-dev-server-is-running">Step 1: Make Sure the Dev Server is Running</h3> <p>If <code>pnpm dev</code> isn't already running from an earlier step, start it now:</p> <pre><code class="language-bash">pnpm run dev # or pnpm dev </code></pre> <p>Plasmo will build the extension into <code>build/chrome-mv3-dev</code> and watch for changes.</p> <h3 id="heading-step-2-load-the-extension-in-chrome">Step 2: Load the Extension in Chrome</h3> <p>If you haven't already loaded the extension, go to <code>chrome://extensions/</code>, enable <strong>Developer mode</strong>, click <strong>Load unpacked</strong>, and select the <code>build/chrome-mv3-dev</code> folder.</p> <p>Once loaded you should see the extension listed with the name "Tab Grouper Tutorial", version "1.0.0", and status Enabled.</p> <h3 id="heading-step-3-pin-the-extension">Step 3: Pin the Extension</h3> <p>Click the puzzle piece icon in the Chrome toolbar, find "Tab Grouper Tutorial", and click the pin icon to keep it visible.</p> <p>The extension icon will now appear directly in your toolbar.</p> <h3 id="heading-step-4-test-the-extension">Step 4: Test the Extension</h3> <h4 id="heading-test-1-open-multiple-tabs">Test 1: Open Multiple Tabs</h4> <p>Open several tabs across a few domains so there's something to group:</p> <ol> <li><p><code>https://github.com/topics</code>, <code>https://github.com/trending</code>, <code>https://github.com/explore</code></p> </li> <li><p><code>https://www.youtube.com/</code> and <code>https://www.youtube.com/trending</code></p> </li> <li><p><code>https://stackoverflow.com/questions</code> and <code>https://stackoverflow.com/tags</code></p> </li> </ol> <p>Have at least 7 tabs open.</p> <h4 id="heading-test-2-group-the-tabs">Test 2: Group the Tabs</h4> <p>Click the Tab Grouper extension icon. The popup should appear showing your open tab count (7 or more) and group count (probably 0).</p> <p>Click <strong>"Group Tabs by Domain"</strong> and watch your tabs get organized into colored groups.</p> <h4 id="heading-test-3-verify-groups">Test 3: Verify Groups</h4> <p>After clicking the button, GitHub tabs should be grouped together with a label like "github.com" and a consistent color, and YouTube tabs similarly.</p> <p>Click the extension icon again, the group count should now show 2, while the tab count stays the same.</p> <h3 id="heading-step-5-debug-the-extension">Step 5: Debug the Extension</h3> <p>If something doesn't work, Chrome's DevTools are your best friend.</p> <p>To inspect the background script, go to <code>chrome://extensions/</code>, find your extension, and click the <strong>"service worker"</strong> link.</p> <p>A DevTools console opens where you can look for the "Tab Grouper background script loaded!" message and any error output in red.</p> <p>To inspect the popup, right-click the extension icon and select <strong>"Inspect popup"</strong>. This opens DevTools for the popup specifically — check the Console tab for any errors there.</p> <p><strong>If nothing happens when you click the button</strong>, check the background script console for errors, confirm you have at least 2 tabs from the same domain, and verify the message is being sent (look in the popup console for any <code>sendMessage</code> failures).</p> <p><strong>If tabs aren't grouping</strong>, double-check that you added the <code>tabs</code> and <code>tabGroups</code> permissions to <code>package.json</code> and reloaded the extension after saving.</p> <p><strong>If you see "Extension cannot access chrome://..."</strong>, that's expected behavior — extensions can't interact with Chrome's internal pages and the code skips them intentionally.</p> <h3 id="heading-step-6-hot-reloading">Step 6: Hot Reloading</h3> <p>One of the benefits of Plasmo is hot reloading, which allows you to update code in a running app instantly without needing to restart it manually.</p> <p>Open <code>popup.tsx</code>, change the header emoji from 🗂️ to 📁, and save.</p> <p>The extension reloads automatically.</p> <p>Click the icon and you'll see the updated emoji immediately.</p> <p>Hot reloading is advantageous because it speeds up development by letting you see changes in real time.</p> <p>You can change the emoji back afterward if you'd like to keep the extension consistent with the rest of the tutorial examples and screenshots.</p> <h3 id="heading-step-7-test-edge-cases">Step 7: Test Edge Cases</h3> <p>It's worth testing a few scenarios to make sure the extension handles them gracefully.</p> <p>If you close all tabs except one and click "Group Tabs", nothing should happen. The extension requires at least two tabs from the same domain to form a group. Opening <code>chrome://extensions</code> and <code>chrome://settings</code> and then grouping should also do nothing, since those pages are filtered out.</p> <p>If you have one tab from <code>reddit.com</code> and one from <code>freecodecamp.org</code>, each domain appearing only once, no groups should be created.</p> <h3 id="heading-step-8-production-build">Step 8: Production Build</h3> <p>When you're ready to share your extension, run:</p> <pre><code class="language-bash">pnpm run build </code></pre> <p>This creates a production-optimized version in <code>build/chrome-mv3-prod</code>, minified JavaScript, no development-only code, and smaller file size.</p> <p>To verify the production build, go to <code>chrome://extensions/</code>, remove the development version, click "Load unpacked", and select <code>build/chrome-mv3-prod</code>. Test thoroughly before publishing.</p> <p>The extension is lightweight (under 100 KB), only runs when you click the button, and has no background processes when idle.</p> <h2 id="heading-next-steps-and-extension-ideas">Next Steps and Extension Ideas</h2> <p>Congratulations on building your first Chrome extension!</p> <p>You now have a working tool that groups tabs by domain with one click, shows live statistics about open tabs and groups, and is built on modern tooling: TypeScript, React, and Plasmo following Chrome extension best practices.</p> <p>The extension is a solid foundation. Here are some ideas for where to take it next.</p> <h3 id="heading-1-auto-grouping">1. Auto-Grouping</h3> <p>Instead of requiring a button click, you could automatically group new tabs as they're opened. You'd listen for the <code>chrome.tabs.onCreated</code> event in <code>background.ts</code> and trigger <code>groupTabsByDomain()</code> with a short delay to let the page URL load:</p> <pre><code class="language-typescript">// In background.ts chrome.tabs.onCreated.addListener(async (tab) =&gt; { // Wait a bit for the URL to load setTimeout(() =&gt; { groupTabsByDomain() }, 2000) }) </code></pre> <p>This gets into event listeners, asynchronous timing, and thinking carefully about when to fire — a good next step for understanding how background scripts can be more proactive.</p> <h3 id="heading-2-keyboard-shortcuts">2. Keyboard Shortcuts</h3> <p>You can trigger grouping without even opening the popup by adding a keyboard shortcut. Add a <code>commands</code> section to the manifest in <code>package.json</code>:</p> <pre><code class="language-json">"manifest": { "commands": { "group-tabs": { "suggested_key": { "default": "Ctrl+Shift+G", "mac": "Command+Shift+G" }, "description": "Group tabs by domain" } } } </code></pre> <p>Then listen for the command in <code>background.ts</code>:</p> <pre><code class="language-typescript">chrome.commands.onCommand.addListener((command) =&gt; { if (command === "group-tabs") { groupTabsByDomain() } }) </code></pre> <h3 id="heading-3-category-based-grouping">3. Category-Based Grouping</h3> <p>Rather than grouping by raw domain, you could group by category — putting GitHub, Stack Overflow, and npm together in a "Dev" group, for instance:</p> <pre><code class="language-typescript">const categories = { social: ["facebook.com", "twitter.com", "instagram.com"], shopping: ["amazon.com", "ebay.com", "etsy.com"], dev: ["github.com", "stackoverflow.com", "npmjs.com"] } function getCategoryForDomain(domain: string): string { for (const [category, domains] of Object.entries(categories)) { if (domains.includes(domain)) { return category } } return "other" } </code></pre> <h3 id="heading-4-options-page">4. Options Page</h3> <p>Plasmo makes it trivial to add a settings page by creating an <code>options.tsx</code> file.</p> <p>This is where you'd let users toggle auto-grouping, choose between domain and category mode, or configure their own category mappings.</p> <p>It's a good introduction to the Chrome Storage API and persisting user preferences.</p> <pre><code class="language-tsx">function OptionsPage() { return ( &lt;div&gt; &lt;h1&gt;Tab Grouper Settings&lt;/h1&gt; &lt;label&gt; &lt;input type="checkbox" /&gt; Enable auto-grouping &lt;/label&gt; &lt;label&gt; &lt;input type="checkbox" /&gt; Group by category instead of domain &lt;/label&gt; &lt;/div&gt; ) } </code></pre> <h3 id="heading-5-tab-age-tracking">5. Tab Age Tracking</h3> <p>You could track when each tab was created and surface tabs that have been sitting untouched for a week or more, a nice way to encourage tab hygiene:</p> <pre><code class="language-typescript">// Track tab creation times const tabCreationTimes = new Map&lt;number, number&gt;() chrome.tabs.onCreated.addListener((tab) =&gt; { if (tab.id) { tabCreationTimes.set(tab.id, Date.now()) } }) // Find old tabs (e.g., &gt; 7 days) function getOldTabs(): chrome.tabs.Tab[] { const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000) return tabs.filter(tab =&gt; { const created = tabCreationTimes.get(tab.id!) return created &amp;&amp; created &lt; sevenDaysAgo }) } </code></pre> <h3 id="heading-6-search-within-groups">6. Search Within Groups</h3> <p>A search bar in the popup would let users filter their open tabs by title, making it easy to jump to a specific tab:</p> <pre><code class="language-tsx">const [searchQuery, setSearchQuery] = useState("") const filteredTabs = tabs.filter(tab =&gt; tab.title?.toLowerCase().includes(searchQuery.toLowerCase()) ) </code></pre> <h3 id="heading-7-exportimport-groups">7. Export/Import Groups</h3> <p>You could let users save their current tab groups to a JSON file and restore them later. Useful for preserving a working session across restarts:</p> <pre><code class="language-typescript">// Export async function exportGroups() { const groups = await chrome.tabGroups.query({}) const data = JSON.stringify(groups) const blob = new Blob([data], { type: 'application/json' }) const url = URL.createObjectURL(blob) chrome.downloads.download({ url, filename: 'tab-groups.json' }) } // Import async function importGroups(file: File) { const text = await file.text() const groups = JSON.parse(text) // Restore groups... } </code></pre> <h3 id="heading-8-group-statistics-dashboard">8. Group Statistics Dashboard</h3> <p>An expanded popup could show browsing analytics, total tabs opened today, most-visited domain, and more:</p> <pre><code class="language-tsx">function Statistics() { const [stats, setStats] = useState({ totalTabs: 0, totalGroups: 0, mostUsedDomain: "", tabsToday: 0 }) return ( &lt;div&gt; &lt;h3&gt;Browsing Statistics&lt;/h3&gt; &lt;p&gt;Total tabs opened today: {stats.tabsToday}&lt;/p&gt; &lt;p&gt;Most visited domain: {stats.mostUsedDomain}&lt;/p&gt; &lt;/div&gt; ) } </code></pre> <h2 id="heading-learning-resources">Learning Resources</h2> <p>If you want to go deeper, the <a href="https://developer.chrome.com/docs/extensions/">official Chrome Extension docs</a> are excellent and cover every API in detail.</p> <p>The <a href="https://github.com/GoogleChrome/chrome-extensions-samples">Chrome Extension Samples repository</a> on GitHub has dozens of real examples to learn from. For Plasmo-specific questions, the <a href="https://docs.plasmo.com/">Plasmo documentation</a> and <a href="https://github.com/PlasmoHQ/examples">example repository</a> are the best starting points, and the community is active on <a href="https://www.plasmo.com/community">Plasmo Discord</a>.</p> <p>The <a href="https://react.dev/">React docs</a> and <a href="https://www.typescriptlang.org/docs/">TypeScript docs</a> are worth bookmarking as reference material, and the <a href="https://react-typescript-cheatsheet.netlify.app/">React TypeScript Cheatsheet</a> is handy when you're unsure about specific type patterns.</p> <p>For community support, Stack Overflow's <code>chrome-extension</code> tag is well-monitored, and r/chrome_extensions on Reddit is a friendly place to ask questions.</p> <h2 id="heading-deploying-to-chrome-web-store">Deploying to Chrome Web Store</h2> <p>Now that you've built and tested your extension, here's how to publish it and share it with the world.</p> <h3 id="heading-what-youll-need">What You'll Need</h3> <p>Before you can publish, you'll need a completed and tested extension, a Google account, a $5 USD one-time developer registration fee, and some store assets such as icons, screenshots, and a written description.</p> <p>The $5 fee is a one-time charge (not annual) that Google uses to verify developer identity and reduce spam. It covers unlimited extension submissions and is processed immediately via Google Payments.</p> <h3 id="heading-step-1-create-a-production-build">Step 1: Create a Production Build</h3> <p>Build your extension for production if you didn't do this before:</p> <pre><code class="language-bash">cd tab-grouper-tutorial npm run build </code></pre> <p>This creates an optimized version in <code>build/chrome-mv3-prod/</code>. The production build minifies JavaScript and CSS for a smaller file size, strips out development-only code and console logs, and optimizes assets for faster loading.</p> <p>Before uploading, load <code>build/chrome-mv3-prod/</code> as an unpacked extension and test all features one more time to confirm nothing broke in the build process.</p> <h3 id="heading-step-2-create-store-assets">Step 2: Create Store Assets</h3> <h4 id="heading-extension-icons">Extension Icons</h4> <p>You'll need icons in three sizes: <strong>128×128 pixels</strong> for the main store listing (required), <strong>48×48</strong> for the extension management page, and <strong>16×16</strong> for use as a favicon.</p> <p>All should be PNG files with transparent backgrounds. Keep the design simple and recognizable at small sizes. Avoid putting text in the 16×16 version.</p> <p><a href="https://figma.com">Figma</a> is free and works well for this, as does <a href="https://canva.com">Canva</a> or <a href="https://gimp.org">GIMP</a>.</p> <h4 id="heading-screenshots">Screenshots</h4> <p>Upload between 1 and 5 screenshots at either 1280×800 or 640×400 pixels (PNG or JPEG).</p> <p>Show the extension in actual use rather than mockups. The popup with statistics, tabs being grouped, and the before/after state all work well.</p> <p>Adding annotations to highlight key features helps users understand what they're looking at.</p> <h4 id="heading-promotional-images-optional">Promotional Images (Optional)</h4> <p>If you want to be featured on the store, you can also upload a small tile (440×280), large tile (920×680), and marquee image (1400×560). These are only needed if Google chooses to promote your extension.</p> <h4 id="heading-demo-video-optional">Demo Video (Optional)</h4> <p>A short YouTube video (30–60 seconds) showing the extension in action can significantly increase conversions. Link to it in your store listing.</p> <h3 id="heading-step-3-write-your-store-listing">Step 3: Write Your Store Listing</h3> <p><strong>Extension Name</strong> (45 character limit): Be clear and descriptive. "Tab Grouper - Organize Tabs by Domain" works well. Avoid keyword stuffing or excessive punctuation.</p> <p><strong>Summary</strong> (132 character limit): This is what appears in search results. Lead with what the extension does: "Automatically organize browser tabs by domain. One-click grouping keeps your workspace clean and productive."</p> <p><strong>Detailed Description</strong> (16,000 character limit): Start with what the extension does, list features clearly, explain how to use it, address privacy, and provide contact information. Here's a template you can adapt:</p> <pre><code class="language-markdown">## What is Tab Grouper? Tab Grouper automatically organizes your browser tabs by grouping them based on their website domain. No more hunting through dozens of tabs - everything is neatly organized. ## Features - ✅ One-click tab grouping - ✅ Automatic color-coding by domain - ✅ Real-time statistics - ✅ Works with all websites - ✅ Lightweight and fast ## How to Use 1. Click the Tab Grouper icon in your toolbar 2. Click "Group Tabs by Domain" 3. Your tabs are instantly organized ## Why You Need This If you regularly have numerous tabs open, finding the right one can waste valuable time. Tab Grouper solves this by automatically organizing tabs into colored groups, making navigation quick and straightforward. ## Privacy This extension does not collect any personal data. It only accesses tab information locally to perform grouping. No data is sent to external servers. ## Support Found a bug or have a suggestion? Contact us at support@example.com </code></pre> <p><strong>Category</strong>: Choose <strong>Productivity</strong> for Tab Grouper. You can add additional languages later if you want to localize the listing.</p> <h3 id="heading-step-4-register-as-a-chrome-web-store-developer">Step 4: Register as a Chrome Web Store Developer</h3> <p>Go to the <a href="https://chrome.google.com/webstore/devconsole">Chrome Web Store Developer Dashboard</a>, sign in with your Google account, accept the Developer Agreement, and pay the $5 registration fee. Your account is activated within minutes.</p> <h3 id="heading-step-5-submit-your-extension">Step 5: Submit Your Extension</h3> <p>In the Developer Dashboard, click <strong>"New Item"</strong> and upload your extension. You can either manually zip the <code>build/chrome-mv3-prod/</code> folder or use Plasmo's package command:</p> <pre><code class="language-bash"># Option 1: Manual zip cd build/chrome-mv3-prod zip -r ../../tab-grouper.zip . # Option 2: Use Plasmo package command cd tab-grouper-tutorial npm run package </code></pre> <p>Once uploaded, fill in all four sections of the store listing form: <strong>Product details</strong> (name, summary, description, category, language), <strong>Graphic assets</strong> (icon and screenshots), <strong>Privacy practices</strong> (see below), and <strong>Distribution</strong> (visibility, regions, pricing).</p> <h4 id="heading-single-purpose-description">Single Purpose Description</h4> <p>Chrome requires each extension to have a single, clearly stated purpose. For Tab Grouper: "This extension organizes browser tabs by grouping them based on their domain name, helping users manage multiple open tabs efficiently."</p> <h4 id="heading-permission-justification">Permission Justification</h4> <p>You'll need to justify each permission you declared. For <code>tabs</code>: "The tabs permission is required to read tab URLs and titles in order to group them by domain." For <code>tabGroups</code>: "The tabGroups permission is required to create and manage tab groups for organization."</p> <h4 id="heading-privacy-policy">Privacy Policy</h4> <p>Even though Tab Grouper doesn't collect personal data, Chrome may require a privacy policy. Host one on GitHub Pages or your personal website and link to it. Here's a minimal template:</p> <pre><code class="language-markdown"># Privacy Policy for Tab Grouper ## Data Collection Tab Grouper does not collect, store, or transmit any personal data. ## Permissions - **tabs**: Used only to read tab URLs for grouping purposes - **tabGroups**: Used only to create and manage tab groups ## Local Processing All tab grouping happens locally in your browser. No data is sent to external servers. ## Contact For questions: your-email@example.com Last updated: [Current Date] </code></pre> <h3 id="heading-step-6-submit-for-review">Step 6: Submit for Review</h3> <p>Before clicking submit, run through this checklist:</p> <ul> <li><p>Production build tested thoroughly</p> </li> <li><p>All store assets uploaded (icon + at least one screenshot)</p> </li> <li><p>Description is clear and accurate</p> </li> <li><p>Permissions are justified</p> </li> <li><p>Privacy policy is linked</p> </li> <li><p>Extension name is descriptive</p> </li> </ul> <p>When you're ready, click <strong>"Submit for review"</strong>, confirm your details, and click <strong>"Publish"</strong>. Your extension enters the review queue.</p> <h3 id="heading-step-7-the-review-process">Step 7: The Review Process</h3> <p>Google typically reviews extensions within 1–3 business days for straightforward submissions, though complex extensions or first submissions can take up to a week. Reviewers check that the extension works as described, that permissions are justified, that there's no malicious code, and that the listing complies with Chrome Web Store policies.</p> <p>You can track your status in the Developer Dashboard: Pending review → In review → Approved or Rejected. If rejected, Google will email you specific reasons and instructions for resubmitting.</p> <p>The most common rejection reasons are insufficient permission justification, misleading descriptions, missing privacy policies, and requesting more permissions than necessary. Address each point in the rejection email, update your submission, and resubmit.</p> <h3 id="heading-step-8-after-approval">Step 8: After Approval</h3> <p>Once approved, your extension is live at <code>https://chrome.google.com/webstore/detail/[extension-id]</code>. Share the link on social media, write a blog post, post to Reddit (r/chrome, r/chrome_extensions), or submit to Product Hunt to drive installs.</p> <p>The Developer Dashboard gives you ongoing analytics — total and weekly installs, reviews and ratings, impressions, and uninstall counts. Check it regularly, especially in the first week. Respond to reviews (particularly negative ones), thank users for positive feedback, and use reported bugs to prioritize future updates.</p> <h3 id="heading-step-9-publishing-updates">Step 9: Publishing Updates</h3> <p>When you fix bugs or add features, bump the version number in <code>package.json</code> (following <a href="https://semver.org/">Semantic Versioning</a> — patch for bug fixes, minor for new features, major for breaking changes), run <code>npm run build</code>, and upload the new package through the Developer Dashboard's <strong>Package</strong> tab. Updates are typically reviewed faster than initial submissions, often within 24 hours.</p> <h3 id="heading-step-10-managing-your-extension-long-term">Step 10: Managing Your Extension Long-Term</h3> <p>The Chrome Web Store provides built-in analytics, but you can also add Google Analytics if you need more detail.</p> <p>For user support, an email address in the description or a GitHub issues page both work well. As you add features, keep the description updated and maintain a changelog so users know what changed and when. Responding to user questions and reviews goes a long way toward building a loyal base of users who'll recommend the extension to others.</p> <h3 id="heading-troubleshooting-common-publishing-issues">Troubleshooting Common Publishing Issues</h3> <p><strong>"Package is invalid" on upload</strong>: Make sure you zipped the contents of <code>build/chrome-mv3-prod/</code> rather than the folder itself, and verify the generated <code>manifest.json</code> is valid JSON.</p> <p><strong>Rejection: Permissions Not Justified</strong>: In the "Permission justification" field, be specific about which feature requires each permission and what would break without it.</p> <p><strong>Rejection: Single Purpose Unclear</strong>: Rewrite the single purpose description to focus on one main function, stated plainly.</p> <p><strong>Low installation rate after launch</strong>: Poor screenshots are often the culprit — they're the first thing most users look at. Make sure they clearly show the extension solving a real problem. Building even a small number of early reviews also makes a big difference to new visitors.</p> <h3 id="heading-alternative-distribution">Alternative Distribution</h3> <p>The Chrome Web Store is the right choice for most public extensions. If you're building an internal tool, an <strong>Unlisted</strong> extension (accessible only via direct link, not searchable) is a good option.</p> <p>If you need to restrict it to users in a specific Google Workspace organization, a <strong>Private</strong> extension is available for that. Self-hosting and sideloading is possible but requires users to enable Developer Mode manually, so it's only practical for very technical audiences.</p> <h2 id="heading-congratulations">Congratulations!</h2> <p>You've gone from an empty folder to a live Chrome extension on the Web Store. Along the way you learned how extensions are structured, how background scripts and popups communicate, how Chrome's tab APIs work, and how to navigate the publishing process end to end.</p> <p>More than any specific API or configuration detail, the most important thing you've built is a mental model for how extensions work and that transfers directly to any extension idea you want to build next.</p> <p>Keep building, keep learning, and keep shipping!</p>

How to apply

  1. 1 Read the full details above and confirm you meet the eligibility criteria.
  2. 2 Prepare your documents — an updated CV, and any cover letter, proposal or certificates required.
  3. 3 Click Apply on official site to complete your application on Preston Mayieka’s official page.
  4. 4 Submit as early as possible — many close once filled.
Apply on official site

Sourced from freecodecamp. Always verify details on the official website. Opportunihub never charges you to apply.

Frequently asked questions

How do I apply for How to Develop Chrome Extensions using Plasmo [Full Handbook]?

Review the full details and eligibility on this page, prepare your documents, then use the “Apply on official site” button to complete your application on Preston Mayieka’s official page.

Is this opportunity remote or location-based?

This opportunity is remote-friendly and open to applicants who can work from anywhere.

Is How to Develop Chrome Extensions using Plasmo [Full Handbook] free to apply for?

Opportunihub lists this Course for free. Legitimate Courses do not ask for payment to apply — never pay a fee to submit an application.