Azim Uddin

How to Build a Chrome Extension with JavaScript

Introduction: What You Need to Know Before Building a Chrome Extension

Chrome extensions are small software programs that customize the browsing experience by adding features, automating tasks, or modifying web pages directly in the Google Chrome browser. They range from simple tools like ad blockers or password managers to complex productivity suites that integrate with third-party APIs. For developers, extensions offer a unique way to reach millions of users without building a standalone application. The core idea is straightforward: you write code that runs inside the browser, interacts with web pages, and responds to user actions. Because Chrome extensions are built with standard web technologies, anyone with basic front-end skills can start creating them today.

What Are Chrome Extensions and How Do They Work?

At their simplest, Chrome extensions are packaged bundles of HTML, CSS, and JavaScript files that the browser loads and executes in a controlled environment. They operate in three distinct layers: the browser UI (via popups, side panels, or toolbar icons), the background context (for long-running logic), and the content of web pages themselves. When you install an extension, Chrome reads a manifest file that declares its permissions, name, version, and which scripts to run. The browser then grants the extension access to specific APIs—like tabs, storage, or network requests—based on those permissions. This sandboxed model ensures extensions cannot interfere with the core browser or other extensions unless explicitly allowed. For example, a price-comparison extension might inject a script into shopping sites to read product prices, then use a background script to fetch competitor data from a server, and finally display results in a popup. All of this happens in real time, without the user leaving the current page.

Core Components: Manifest, Background Scripts, and Content Scripts

Every Chrome extension relies on three essential building blocks, each with a distinct role:

  • Manifest (manifest.json): The configuration file that defines the extension’s identity, permissions, and entry points. It must include at least the manifest version (currently 3), the extension name, and a version number. You can also specify icons, default popup, and which scripts to load.
  • Background Scripts (service worker): A persistent or event-driven script that handles browser-level events, such as clicks on the toolbar icon, keyboard shortcuts, or messages from content scripts. In Manifest V3, this is a service worker that can be terminated when idle to save resources.
  • Content Scripts: JavaScript files that run in the context of web pages. They can read and modify the DOM, but they operate in an isolated world, meaning they cannot access page variables directly unless you use specific APIs. These scripts are declared in the manifest with a list of URL patterns they should run on.

Together, these components communicate using Chrome’s messaging APIs. For instance, a content script might detect a form submission and send a message to the background script, which then stores data or triggers a network request. The UI layer—typically an HTML popup or options page—uses the same messaging to update the interface based on background results.

Prerequisites: JavaScript Skills and Tools You’ll Need

Before you write your first line of extension code, ensure you have the following foundation:

Requirement What You Need Why It Matters
JavaScript fundamentals Variables, functions, event listeners, promises, and basic DOM manipulation. All extension logic—from reacting to clicks to fetching data—is written in JavaScript.
HTML & CSS basics Ability to structure a simple page (popup or options) and style it. User-facing interfaces are just web pages, so standard markup and styling apply.
Chrome browser Latest stable version of Google Chrome installed. You’ll test and debug directly in the browser’s extension environment.
Code editor Any text editor, but VS Code, Sublime Text, or Atom are recommended. You’ll write multiple files and need syntax highlighting and file navigation.
Basic command-line comfort Optional, but useful for zipping files or running a local dev server. Packaging and loading unpacked extensions may require simple file operations.

No prior experience with Chrome’s internal APIs is required—you’ll learn those as you go. Having a clear idea of what your extension should do (e.g., “highlight all links on a page” or “save tabs to a list”) will keep your learning focused. Once you have these prerequisites in place, you’re ready to create your first manifest and start coding.

Setting Up Your Development Environment

Before writing a single line of JavaScript, you need a reliable workspace. A well-prepared environment prevents common pitfalls—like debugging a typo in a file path or struggling to reload an unpacked extension—and sets the foundation for clean, maintainable code. This guide walks you through the essential steps: choosing your tools, organizing your project, enabling Chrome’s Developer Mode, and creating your first manifest file.

Choosing a Code Editor and Project Structure

Your choice of code editor is personal, but for extension development, you need syntax highlighting, file-tree navigation, and terminal access. Visual Studio Code is the most popular option due to its built-in JavaScript support and extensions like “Chrome DevTools” or “Prettier.” Alternatives include Sublime Text (fast and lightweight) or JetBrains WebStorm (feature-rich but heavier). Whichever you choose, install a linter (e.g., ESLint) to catch errors early.

Once your editor is ready, create a dedicated project folder. Avoid scattering files on your desktop—a single root directory for each extension keeps everything self-contained. Here is a recommended structure for a simple extension:

  • manifest.json – the heart of your extension (required).
  • background.js – for service workers or background scripts.
  • content.js – for scripts that run on web pages.
  • popup/ – a subfolder containing popup.html, popup.css, and popup.js for the toolbar interface.
  • icons/ – folder with PNG icons (16×16, 48×48, 128×128).
  • styles/ – optional, for shared CSS.

This separation of concerns means you never mix page-injected code with popup logic. For a first project, start with only manifest.json and one script—simplicity reduces confusion.

Enabling Developer Mode in Chrome

Chrome does not allow installing extensions from outside the Web Store by default. Developer Mode unlocks the “Load unpacked” button, which lets you test your code locally. Here is how to enable it:

  1. Open Chrome and navigate to chrome://extensions in the address bar.
  2. In the top-right corner, toggle the Developer mode switch to ON.
  3. You will immediately see new buttons appear: Load unpacked, Pack extension, and Update.

After enabling it, click Load unpacked and select your project folder. The extension will appear in the list, and you can reload it anytime by clicking the circular arrow icon on its card. A common mistake is forgetting to reload after editing manifest.json—always refresh the extension page to apply changes.

Creating Your First Manifest File

The manifest.json file is the configuration blueprint that Chrome reads to understand your extension’s name, permissions, and behavior. Without it, Chrome will reject your folder. Use Manifest V3 (the current standard) for new projects. Here is a minimal working example:

{
  "manifest_version": 3,
  "name": "My First Extension",
  "version": "1.0",
  "description": "A simple extension that logs a message.",
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup/popup.html"
  },
  "permissions": ["activeTab"]
}

Key fields to note: manifest_version must be 3; name and version are mandatory; background.service_worker points to your background script; action.default_popup defines the popup interface; and permissions declares which Chrome APIs you need. Start with activeTab—it gives temporary access to the current tab without broad permissions. Save this file in your project root, then load the folder via the “Load unpacked” button. If everything is correct, you will see your extension’s name appear in the toolbar.

With these three pieces in place—editor, folder structure, and a valid manifest—you are ready to write your first JavaScript logic. The next step is to build a background script that responds to browser events, but that requires a solid foundation, which you now have.

Understanding the Manifest File (Manifest V3)

The manifest.json file is the single source of truth for any Chrome extension. It declares the extension’s identity, capabilities, and the resources it can access. Without a valid manifest, Chrome will refuse to load the extension. In Manifest V3 (MV3), this file has become more structured and security-focused, requiring developers to explicitly declare permissions and use service workers instead of background pages. Below, we break down the essential fields, permission models, and the key differences from the older Manifest V2.

Required Fields: name, version, and manifest_version

Every manifest must include three mandatory fields. Omitting any of them causes an immediate load error.

  • name (string): The display name of your extension, shown in the toolbar, extensions page, and Chrome Web Store. Keep it under 45 characters for store compliance.
  • version (string): A semantic version string (e.g., "1.0.0") that Chrome uses for update checks. It must consist of one to four dot-separated integers.
  • manifest_version (integer): Set to 3 for MV3. This tells Chrome which API and permission model to apply. Using 2 is deprecated and will be blocked in future releases.

Here is a minimal valid manifest:

{
  "manifest_version": 3,
  "name": "Example Extension",
  "version": "1.0.0"
}

Declaring Permissions and Host Permissions

Permissions are the gatekeepers of user data and browser features. In MV3, you declare them in two separate arrays:

  • permissions: API-level capabilities (e.g., "storage", "alarms", "tabs"). These do not require a specific website URL.
  • host_permissions: URL patterns that grant access to specific domains or all sites (e.g., "https://api.example.com/*" or "<all_urls>"). These control cross-origin fetches, content script injection, and cookie access.

Example with both:

{
  "manifest_version": 3,
  "name": "Example with Permissions",
  "version": "1.0.0",
  "permissions": ["storage", "tabs"],
  "host_permissions": ["https://api.example.com/*"]
}

Minimizing permissions is critical: users see a warning list during installation, and excessive permissions reduce trust and install rates.

Manifest V3 vs V2: What Changed and Why It Matters

MV3 is not a cosmetic update; it fundamentally changes how extensions execute code. The most important shift is replacing persistent background pages with event-driven service workers, which are terminated when idle to reduce resource usage. Additionally, remote code is banned—all scripts must be bundled locally. The table below summarizes the core differences.

Aspect Manifest V2 Manifest V3
Background execution Persistent background page (always running) Non-persistent service worker (starts on events, stops when idle)
Code execution Allows remotely hosted code Only locally bundled scripts; no remote code
Permissions Single permissions array for both APIs and hosts Separate permissions and host_permissions arrays
API access chrome.extension and chrome.browserAction Uses chrome.action and Promise-based APIs

These changes matter because they improve browser performance, security, and user privacy. As a developer, you must adopt MV3 to ensure your extension remains installable and functional. The service worker model forces you to manage state carefully—use chrome.storage instead of global variables—and to register event listeners at the top level of the worker script. Understanding these constraints early will save you from debugging silent failures later. Always test your manifest with Chrome’s extension error console, and keep your permissions as narrow as possible to build a trustworthy, efficient extension.

Building the Background Service Worker

The background service worker is the central nervous system of your Chrome extension. It runs in the background, independent of any specific webpage or popup, and is responsible for handling extension-wide events such as toolbar icon clicks, keyboard shortcuts, or messages from content scripts. Unlike older background pages, service workers are event-driven and can be terminated when idle to save system resources, then restarted when an event fires. This makes them efficient but requires you to write stateless code that does not rely on persistent global variables.

What Is a Service Worker and Why Use It?

A service worker is a JavaScript file that runs in a separate context from your extension’s UI. It does not have access to the DOM, but it can use all Chrome extension APIs, including chrome.runtime, chrome.storage, and chrome.tabs. You declare it in your manifest.json under the background key, specifying the script and that it should be registered as a service worker (Manifest V3 requires this format):

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "background": {
    "service_worker": "background.js"
  }
}

Why use a service worker instead of a persistent background page? Three key reasons:

  • Resource efficiency: It unloads when idle, reducing memory and CPU usage on users’ machines.
  • Modern architecture: Aligns with web platform standards and improves security by isolating background logic.
  • Event-driven reliability: It wakes up exactly when needed, ensuring your extension responds to user actions without constant overhead.

Registering Event Listeners for Extension Lifecycle

Your service worker must register listeners for lifecycle events such as installation, update, and startup. The most common are chrome.runtime.onInstalled (fires on first install or update) and chrome.runtime.onStartup (fires when the browser starts). Use these to initialize state, set default settings, or create context menus. Here is a practical example that logs installation and sets a default value in storage:

// background.js
chrome.runtime.onInstalled.addListener((details) => {
  console.log(`Extension installed or updated: ${details.reason}`);
  chrome.storage.sync.set({ enabled: true }, () => {
    console.log('Default settings initialized.');
  });
});

chrome.runtime.onStartup.addListener(() => {
  console.log('Browser started. Service worker active.');
});

Note that you must register these listeners at the top level of your script, not inside asynchronous callbacks, because the service worker may be terminated and restarted at any time. Any listener not registered synchronously will be lost when the worker restarts.

Communicating with Other Parts of the Extension

The service worker acts as a message hub between different extension components—popups, content scripts, options pages, and devtools. Use chrome.runtime.sendMessage to send a message from any of these contexts, and listen with chrome.runtime.onMessage in the service worker. The listener receives the message, a sender object, and a sendResponse callback for synchronous replies. Here is an example that responds to a toolbar icon click and forwards data to content scripts:

// background.js
chrome.action.onClicked.addListener((tab) => {
  console.log(`Toolbar icon clicked on tab: ${tab.id}`);
  chrome.tabs.sendMessage(tab.id, { action: 'toggleHighlight' }, (response) => {
    if (chrome.runtime.lastError) {
      console.warn('Content script not available:', chrome.runtime.lastError.message);
    } else {
      console.log('Content script responded:', response);
    }
  });
});

// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getStatus') {
    chrome.storage.sync.get('enabled', (data) => {
      sendResponse({ enabled: data.enabled });
    });
    return true; // Keep the message channel open for async response
  }
});

When responding asynchronously (e.g., after reading from storage), you must return true from the listener to signal that you will call sendResponse later. For one-way communication, you can omit the response callback entirely. Always check chrome.runtime.lastError when sending messages to tabs that may not have a content script injected, as shown above.

In practice, keep your service worker lean—delegate heavy work to content scripts or offscreen documents. Use chrome.storage for persistent state, and always re-register listeners on every script load. This ensures your extension remains responsive and crash-free, even after the service worker is recycled by the browser.

How to Build a Chrome Extension with JavaScript: Creating a Popup Interface with HTML and CSS

When you click a Chrome extension’s toolbar icon, the small window that appears is its popup. This interface is your primary point of direct user interaction, so building it well matters. The popup is essentially a standalone HTML document, but it has unique constraints: it closes when the user clicks outside it, and its height is capped at 600 pixels. In this guide, you will create a functional note-taking popup that saves text to Chrome’s storage, demonstrating the core workflow of linking structure, style, and logic.

Designing the Popup HTML Layout

Start with a minimal HTML file named popup.html. The document requires a <!DOCTYPE html> declaration, a <head> with a <title>, and a <body>. Because the popup is small, keep the layout compact. A typical note-taking popup contains three elements: a text area for input, a save button, and a status message. Use semantic tags and avoid nested containers beyond necessity.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Quick Note</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <textarea id="note-input" placeholder="Type your note here..." rows="5"></textarea>
  <button id="save-note">Save Note</button>
  <p id="status"></p>
  <script src="popup.js"></script>
</body>
</html>

This layout prioritizes function. The textarea accepts multi-line text, the button triggers saving, and the p element provides feedback. Keep the structure flat; unnecessary wrappers complicate styling and reduce readability.

Adding Interactivity with JavaScript

The logic in popup.js handles two tasks: loading any previously saved note and saving the current note. Use the chrome.storage.local API for persistence. First, retrieve the saved note when the popup opens. Second, attach a click event listener to the button. Here is the complete script:

const input = document.getElementById('note-input');
const saveButton = document.getElementById('save-note');
const status = document.getElementById('status');

// Load existing note on popup open
chrome.storage.local.get(['note'], (result) => {
  if (result.note) {
    input.value = result.note;
  }
});

// Save note on button click
saveButton.addEventListener('click', () => {
  const note = input.value.trim();
  if (note) {
    chrome.storage.local.set({ note }, () => {
      status.textContent = 'Note saved!';
      setTimeout(() => { status.textContent = ''; }, 1500);
    });
  } else {
    status.textContent = 'Note is empty.';
  }
});

Notice the use of chrome.storage.local.get with a callback. This asynchronous pattern is standard for extension APIs. The trim method prevents saving whitespace-only entries. The status message clears itself after 1.5 seconds to avoid clutter.

Styling the Popup for a Polished Look

CSS in popup.css should define a clear visual hierarchy. Set a fixed width (e.g., 300px) to prevent layout shifts. Use padding and margin to create breathing room. Style the textarea to resize only vertically, and give the button a distinct hover state. Below is a complete stylesheet that achieves a clean, modern appearance.

body {
  width: 300px;
  padding: 16px;
  font-family: Arial, sans-serif;
  background-color: #f9f9f9;
}

textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 10px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  resize: vertical;
  min-height: 80px;
}

button {
  width: 100%;
  margin-top: 12px;
  padding: 10px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
}

button:hover {
  background-color: #45a049;
}

#status {
  margin-top: 8px;
  font-size: 12px;
  color: #666;
  text-align: center;
}

Key styling decisions include using box-sizing: border-box so padding does not expand the textarea beyond the popup width. The button’s full-width layout makes it easy to click. The status color is muted to avoid distracting from the primary content.

Once these three files are in place, update your extension’s manifest.json to include the popup path:

{
  "manifest_version": 3,
  "name": "Quick Note",
  "version": "1.0",
  "action": {
    "default_popup": "popup.html"
  },
  "permissions": ["storage"]
}

After reloading the extension in chrome://extensions, clicking the toolbar icon opens your polished, functional popup. This pattern—separate HTML, CSS, and JavaScript files—scales well for any popup-based tool, from text manipulators to quick calculators.

Injecting Content Scripts to Interact with Web Pages

Content scripts are the bridge between your Chrome extension and the live content of a web page. They run in an isolated world, meaning they can access and manipulate the Document Object Model (DOM) of the page—reading text, altering styles, adding elements—without conflicting with the page’s own JavaScript. This makes them essential for features like text highlighting, grammar checking, or injecting custom tooltips. However, they cannot directly call APIs from the background service worker or access extension storage without messaging.

What Are Content Scripts and What Can They Do?

Content scripts are JavaScript files that execute in the context of a web page, not in the extension’s background environment. Their primary power lies in DOM access: you can query elements, listen to user interactions (clicks, scrolls, keypresses), and modify the page in real time. For example, a script can loop through all paragraph tags and change their background color, or it can detect a selected text and wrap it in a highlighted span. They also have limited access to a subset of Chrome APIs, such as chrome.runtime.sendMessage to communicate with the background script, but they cannot use chrome.tabs or chrome.storage directly. Key limitations include:

  • No access to page’s JavaScript variables or functions (isolated world).
  • Cannot load external scripts from remote servers unless declared in web_accessible_resources.
  • Cannot use most extension APIs; must message the background for heavy tasks.

Use content scripts when you need to interact with page content. Use background scripts (service workers) for handling browser-level events, managing tabs, or performing network requests that should persist across page reloads.

Injecting Scripts via Manifest Declaration

The simplest method is to declare content scripts in your manifest.json file. This is ideal for scripts that should run automatically on every page matching a URL pattern. Here is an example manifest snippet:

{
  "manifest_version": 3,
  "name": "Page Highlighter",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["https://*/*", "http://*/*"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_idle"
    }
  ]
}

In this setup, content.js runs after the page finishes loading (document_idle). You can also specify document_start or document_end for earlier or later execution. The matches field controls which URLs trigger the script. This declarative approach is efficient because the browser handles injection automatically, and it works without user action. However, it is static—you cannot conditionally inject based on runtime logic.

Programmatic Injection with chrome.scripting API

For dynamic control, use the chrome.scripting API from your background script. This is useful when you want to inject a script only after a user clicks a toolbar icon, or only on a specific tab. You must add the "scripting" permission to your manifest. Here is a practical example that changes the page background when a user clicks the extension action:

// background.js
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      document.body.style.backgroundColor = '#f0f8ff';
    }
  });
});

This injects an inline function into the active tab. You can also inject an external file using files: ['content.js'] instead of func. Programmatic injection is ideal for one-off actions, user-triggered features, or when you need to pass runtime data (e.g., a user-selected color) into the script. To highlight text, you could send a message from the content script to the background, which then executes a highlighting function—or you can directly call executeScript with a function that wraps selected text in a <mark> element. Remember that programmatic injection requires the tab to be fully loaded, and you must handle errors if the tab is inaccessible (e.g., a Chrome internal page).

In practice, combine both methods: declare static scripts for universal features, and use chrome.scripting for conditional, user-driven interactions. This keeps your extension responsive and avoids unnecessary memory usage on pages where injection is not needed.

Messaging Between Extension Parts

Chrome extensions are composed of isolated contexts: the popup, content scripts, and the background service worker each run in their own JavaScript environment. To exchange data—such as a popup requesting page metadata from a content script—you must use the extension messaging API. This system is asynchronous by design, ensuring that a slow receiver does not block the sender. The two core methods are chrome.runtime.sendMessage for sending to the extension’s own contexts (popup, background, options) and chrome.tabs.sendMessage for sending specifically to content scripts running in a given tab. Both return a Promise that resolves with the response from the receiver, or you can use a callback for older code.

One-Time Messages with sendMessage and onMessage

The simplest pattern is a one-time request. The sender calls sendMessage with a payload (typically an object with a type field to route the action). The receiver listens via chrome.runtime.onMessage or chrome.tabs.onMessage (for content scripts). The listener receives three arguments: message, sender (which includes tab and frame info), and a sendResponse function. To send a reply, you must call sendResponse synchronously or return true from the listener to keep the message channel open for asynchronous work. If you do not return true, the channel closes immediately after the listener finishes, so any delayed response will fail.

Sending Messages from Popup to Content Script

Consider a popup that needs the page’s title and URL. The popup cannot access the DOM directly; it must ask the content script. First, get the active tab using chrome.tabs.query({active: true, currentWindow: true}). Then call chrome.tabs.sendMessage(tab.id, {type: 'GET_PAGE_DATA'}). On the content script side, listen with chrome.runtime.onMessage.addListener (content scripts receive runtime messages from the extension). The listener checks the type, gathers document.title and location.href, and calls sendResponse({title, url}). The popup’s sendMessage call resolves with that object. A critical caveat: if the content script is not injected into the page (e.g., blocked by permissions or the page is a chrome:// URL), sendMessage rejects with an error. Always handle this rejection.

Handling Responses and Error Cases

Because messaging is asynchronous, you must handle both success and failure. Use async/await in modern extensions:

// popup.js
const tab = (await chrome.tabs.query({active: true, currentWindow: true}))[0];
try {
  const data = await chrome.tabs.sendMessage(tab.id, {type: 'GET_PAGE_DATA'});
  console.log(data.title, data.url);
} catch (err) {
  console.error('Content script not available:', err.message);
}

Common error cases include:

  • No receiver: The content script is not injected (e.g., on restricted pages). The promise rejects with “Could not establish connection. Receiving end does not exist.”
  • Async response: If the listener performs a fetch or uses a timeout, it must return true from the listener and call sendResponse later. Otherwise, the response is lost.
  • Multiple listeners: If more than one listener exists for the same message type, only the first one that calls sendResponse wins. Subsequent calls are ignored.
  • Serialization: Messages are JSON-serialized. You cannot send functions, Date objects, or Map/Set instances directly.

For robust design, always validate the response shape in the sender. If the content script returns undefined (because it didn’t recognize the message type), the promise resolves with undefined, which can cause silent bugs. Prefer using a type switch in the listener and returning a consistent response object, even for unknown types, e.g., {error: 'unknown type'}. This makes debugging predictable and keeps the messaging layer decoupled from UI logic. Finally, remember that the background service worker can also receive runtime messages from the popup or content scripts—use the same onMessage pattern there for global actions like opening a new tab or fetching remote data.

How to Build a Chrome Extension with JavaScript: Adding Options and Persisting Data

Once your extension performs its core tasks, users will expect to customize its behavior. An options page provides a clean interface for these preferences, while chrome.storage ensures that settings survive browser restarts and synchronize across devices when appropriate. This section walks through creating an options page and managing data effectively.

Building an Options Page with HTML and JavaScript

An options page is a standard HTML document registered in your manifest file. Add "options_page": "options.html" to your manifest.json (or "options_ui" for modern versions). The page can include any input elements, but it must include a script to handle saving and loading.

Here is a minimal example with a toggle and a text input:

<!-- options.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
  <label>
    <input type="checkbox" id="darkMode"> Enable dark mode
  </label>
  <br>
  <label>
    <input type="text" id="apiKey" placeholder="Enter API key">
  </label>
  <br>
  <button id="save">Save</button>
  <script src="options.js"></script>
</body>
</html>


// options.js
const defaultSettings = { darkMode: false, apiKey: '' };

document.addEventListener('DOMContentLoaded', () => {
  chrome.storage.sync.get(defaultSettings, (items) => {
    document.getElementById('darkMode').checked = items.darkMode;
    document.getElementById('apiKey').value = items.apiKey;
  });
});

document.getElementById('save').addEventListener('click', () => {
  const settings = {
    darkMode: document.getElementById('darkMode').checked,
    apiKey: document.getElementById('apiKey').value.trim()
  };
  chrome.storage.sync.set(settings, () => {
    console.log('Settings saved');
  });
});

Notice that loading uses chrome.storage.sync.get with a default object. This pattern ensures that missing keys are populated with sensible defaults, preventing undefined errors elsewhere.

Using chrome.storage.sync vs chrome.storage.local

Chrome provides two storage areas, each with distinct trade-offs. The table below compares their key characteristics:

Feature chrome.storage.sync chrome.storage.local
Data synchronization Synced across all signed-in Chrome browsers Local to the current browser profile only
Default quota 100 KB per item, 8 KB per key (as of Chrome 114) 10 MB (can be increased with unlimitedStorage permission)
Write frequency Rate-limited (approximately 120 writes per minute) No practical write limit
Best use case User preferences, small settings, cross-device needs Large data, caches, per-machine state, sensitive info

Choose sync for settings users expect to follow them across devices, such as theme preferences or default language. Choose local for data that is bulky, ephemeral, or specific to one machine—for example, a cached list of recently viewed items or a large user-generated dataset. Note that sync has strict quota limits; exceeding them will throw errors, so always handle write failures gracefully.

Reading and Writing Settings from Other Parts

Any extension component—background script, content script, or popup—can access the same chrome.storage areas. This shared access is what makes settings effective across the entire extension. Use chrome.storage.sync.get with a callback or a Promise (if using Chrome 88+ with chrome.storage.promise).

For example, in a content script that needs the dark mode setting:

// content.js
chrome.storage.sync.get('darkMode', (result) => {
  if (result.darkMode) {
    document.body.classList.add('dark-theme');
  }
});

To react to changes made in the options page while other parts are open, listen to the chrome.storage.onChanged event. This is particularly useful for live updates without requiring a reload:

// background.js
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync' && changes.darkMode) {
    // Broadcast to all tabs or update internal state
  }
});

When writing settings, always use the same storage area consistently. Mixing sync and local for the same setting will lead to confusion. A recommended pattern is to store all user-facing preferences in sync, and any derived or cached data in local. This separation keeps the architecture predictable and makes debugging straightforward.

Testing and Debugging Your Extension

Once you’ve written your extension’s code, systematic testing is essential. A single typo in a manifest file or an improperly scoped permission can break the entire extension without obvious feedback. Follow a structured workflow: load the extension fresh, verify background and content scripts independently, and then test user interactions. This approach isolates failures and reduces debugging time.

Loading an Unpacked Extension and Reloading Changes

Start by opening Chrome and navigating to chrome://extensions. Enable “Developer mode” using the toggle in the top-right corner. Click “Load unpacked” and select your extension’s folder containing the manifest.json file. The extension appears immediately, but any changes you make to its files require a manual reload. Click the circular arrow icon on your extension’s card to reload it. For faster iteration, use the keyboard shortcut Ctrl+R (Windows/Linux) or Cmd+R (macOS) while focused on the extension card.

When reloading, remember that the extension’s state resets—background script variables are cleared, and content scripts are re-injected only on new page loads. If you modify the manifest (e.g., adding permissions), you must click “Load unpacked” again or use the “Reload” button, as Chrome does not auto-detect manifest changes. To avoid stale versions, always reload the extension and then refresh any open tabs where content scripts run.

Debugging Background Scripts and Content Scripts

Background scripts (including service workers) have their own console. On the chrome://extensions page, find your extension and click the “service worker” link under “Inspect views.” This opens DevTools attached to the background context. Use console.log() statements here to trace event listeners, API calls, and state changes. Set breakpoints in the Sources panel to step through async code, inspecting the call stack and variable values.

Content scripts are trickier because they run in the page’s context. Open DevTools on any webpage (right-click → Inspect). Your content script’s logs appear in the Console, but only if you select the correct execution context from the dropdown at the top—choose your extension’s ID. To debug step-by-step, add a debugger; statement in your content script. Reload the page, and execution pauses. Then use the Sources panel to walk through each line, examine the DOM, and verify that your script receives the correct data from the background script via chrome.runtime.sendMessage.

Common Errors and How to Fix Them

These issues appear frequently and are easy to diagnose:

Error Symptom Likely Cause Solution
“Permission denied” in console Missing host_permissions or permissions in manifest Add the required domain patterns (e.g., "https://*/*") and reload
Extension not working after code change Stale version loaded Click “Reload” on the extension card, then refresh test pages
Content script not injected Manifest matches pattern too narrow Check content_scripts.matches; use "" for testing
Service worker inactive Chrome terminates idle workers Use chrome.alarms or keep a port open; debug via “Inspect views”
Message not received Async race condition Await sendResponse and return true in the listener

For step-by-step debugging, use the “Sources” panel in either DevTools instance. Set breakpoints on line numbers, then trigger your extension’s action (e.g., clicking the toolbar icon). Watch the Scope pane for variable values. When a permission error occurs, Chrome logs the exact missing API name—search for it in the manifest and add it under "permissions". Always test in a fresh Chrome profile (no other extensions) to eliminate interference. Finally, keep the extension’s ID consistent by using "key" in the manifest—this prevents state loss during development.

Publishing Your Extension to the Chrome Web Store

Once your extension is polished and tested locally, the final step is distribution. The Chrome Web Store is the official marketplace where users discover, install, and update extensions. Publishing there ensures your work reaches a global audience with built-in trust and security. This process involves account setup, asset preparation, and a review cycle that keeps the ecosystem safe.

Creating a Chrome Web Store Developer Account

Before uploading any code, you must register as a developer. This is a one-time, straightforward process:

  • Sign in with a Google account you intend to use for all developer communications.
  • Pay the one-time registration fee of $5 USD. This fee is non-refundable and acts as a barrier to spam.
  • Accept the developer agreement, which outlines content policies, user data handling, and prohibited practices.
  • Set up a developer profile with a public display name and a verified email address for user support.

After registration, you gain access to the Chrome Web Store Developer Dashboard, where you manage all your extensions.

Preparing Required Assets and Promotional Images

Your listing’s visual quality directly impacts conversion. The store requires specific assets, and missing any will block submission. Use the following checklist:

Asset Required Size (px) Purpose
Store icon 128 × 128 Displayed in search results and install dialogs.
Small promotional tile 440 × 280 Used in category listings and carousels.
Large promotional tile 920 × 680 Featured placement on the store homepage.
Screenshots 1280 × 800 or 800 × 1280 At least 1; up to 5 showing UI and functionality.
Detailed description Text only, max 16,000 chars Explain features, permissions, and usage clearly.

Also prepare a privacy policy URL if your extension collects any user data—even anonymized analytics. The policy must be hosted on a live page and clearly state what data is collected, why, and how it is protected. Without this, your submission will be rejected.

Submitting for Review and Managing Updates

With assets ready, upload your extension’s ZIP file (containing the manifest, scripts, and icons) to the dashboard. Fill out the listing details, select a category, and choose a visibility option (public, unlisted, or private). Then submit for review.

The review process typically takes between a few hours and several days. Google’s automated and manual checks verify compliance with security, privacy, and functionality standards. If issues arise, you’ll receive a rejection reason and can resubmit after fixing the problem.

Once approved, your extension goes live. Managing updates is equally important:

  • Bump the version field in your manifest.json for every change.
  • Upload the new ZIP and add release notes describing what’s new or fixed.
  • Submit for another review—updates go through the same vetting process.
  • Monitor user feedback and crash reports in the dashboard to prioritize future improvements.

Publishing is not the end but the beginning of your extension’s lifecycle. Regular updates, clear communication about permissions, and a transparent privacy policy build long-term user trust and ensure your extension remains listed and relevant.

Frequently Asked Questions

What is the minimum required file for a Chrome extension?

The minimum required file is a manifest.json file. This file defines the extension's name, version, description, permissions, and other metadata. For a basic extension, you also need a JavaScript file for the service worker (background script) and optionally an HTML file for a popup. The manifest is the heart of the extension and must be valid JSON.

What is Manifest V3 and why is it important?

Manifest V3 (MV3) is the latest version of the Chrome extension manifest specification. It replaces background pages with service workers, uses promises for many APIs, and enforces stricter security rules. MV3 improves performance and security. All new extensions must use MV3, and Chrome is phasing out older versions. Developers should follow MV3 guidelines.

How do I communicate between a popup and a content script?

You communicate using Chrome's message passing API. From the popup or background, you can use chrome.tabs.sendMessage() to send a message to a content script in a specific tab. Content scripts can respond using chrome.runtime.onMessage listener. Alternatively, you can use chrome.runtime.connect() for long-lived connections. Always handle errors and verify the tab.

Can I use modern JavaScript features in a Chrome extension?

Yes, Chrome extensions support modern JavaScript features like async/await, destructuring, and modules. For service workers, you can use ES modules if you declare them in the manifest. Content scripts also support modern syntax. However, be mindful of the Chrome version your users have. Use transpilation if you need to support older browsers.

How do I store data persistently in a Chrome extension?

Use chrome.storage API. The chrome.storage.local provides unlimited storage (with user consent) for your extension's data. You can also use chrome.storage.sync to sync data across devices if the user is signed in. Use async methods like storage.get() and storage.set(). Avoid using localStorage because it's not shared across contexts.

What permissions do I need for a basic extension?

For a basic extension that only modifies the current page, you may need 'activeTab' permission. This grants temporary access to the current tab when the user invokes the extension. If you need to inject scripts on all pages, you need 'scripting' and host permissions. Always request the minimum permissions required for your functionality.

How do I test a Chrome extension during development?

Go to chrome://extensions in Chrome, enable Developer mode, and click 'Load unpacked'. Select your extension folder. After making changes, click the refresh icon on your extension card to reload it. Use the service worker console for debugging background scripts and the page console for content scripts. You can also use breakpoints.

How do I publish my extension to the Chrome Web Store?

Create a developer account on the Chrome Web Store Developer Dashboard (one-time fee). Package your extension as a ZIP file (including all files). Upload the ZIP, provide a detailed description, screenshots, and promotional images. Set a privacy policy if required. Submit for review. Once approved, it becomes publicly available.

Sources and further reading

Need help with this topic?

Send us your details and we will contact you.

    Leave a Reply

    Your email address will not be published. Required fields are marked *