Skip to main content
  1. Posts/

How to create a Chrome Extension with Manifest V3

·8 mins
Alejandro AO
Alejandro AO
I’m a software engineer building AI applications. I publish weekly video tutorials where I show you how to build real-world projects. Feel free to visit my YouTube channel or Discord and join the community.

In this article, we will see how to create a Chrome extension. The extension we will build uses the latest version of the Chrome Manifest (manifest.json), which is V3. If you don’t know what is the Manifest, don’t worry. We will see that in a second.

Also, if you want to follow along, here is the video version of this tutorial where i build an extension for ChatGPT 👇

Install the boilerplate (or create one)

To start from an already configured boilerplate, you can download it from this GitHub repository. This boilerplate will help you get started faster. Let me explain to you real quick how it works.

A Chrome extension’s folder structure is a little different than that of a regular application. In this boilerplate, all the extension code is contained in the folder “extension”. This is the folder you must load to your browser or the Chrome Web Store. The files outside this folder are just the bundler (Webpack) and your NPM packages. Here is the folder structure:

├── extension/
│   ├── background.js
│   ├── content.js
│   ├── icons/
│   │   ├── icon16.png
│   │   ├── icon32.png
│   │   ├── icon48.png
│   │   └── icon128.png
│   ├── popup/
│   │   ├── popup.html
│   │   └── popup.js
│   └── manifest.json
├── src/
│   └── // source files for the extension
├── webpack.config.js
├── babel.config.js
├── package.json
└── node_modules/
    └── // dependencies installed by npm

Load your extension locally

In order to start testing your extension locally, you will need to load it to your Chrome browser. This is very easy to do in Google Chrome. Just go to your extensions manager at chrome://extensions and enable the developer mode (top-right corner).

This will show a new button at the top-left titled “Load unpacked”. Click on that button and select the “extension” folder of your extension. Remember that it is the “extension” file that actually contains the extension and not the upper folder with all the bundler configurations.

Now you are all set. Just remember to reload your extension by clicking on the reload button every time you update the code.

What does each file do in the Chrome Extension?

As we mentioned before, the files outside the folder “extension” are only used to bundle the content script. These files include a Webpack configuration file with necessary scripts that you can use at will. The files inside the extension itself are the following:


According to Google,

An extension  manifest gives the browser information about the extension, such as the most important files and the capabilities the extension might use.

This is a JSON file where you define the name of the extension, its permissions (the user’s data that it will have access to), and the files that it will load. In the example below, the extension requires the “tabs API” permission, which will allow us to get information on the current browser tabs and send requests to them.

This file has to be titled manifest.jsonfor Chrome to recognize it. A usual manifest.json file looks like this:

  "manifest_version": 3,
  "name": "The name of your extension",
  "description": "The description of your extension",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon16.png",
    "32": "images/icon32.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  "background": {
    "service_worker": "background.js"
  "action": {
    "permissions": ["tabs"],
		"default_popup": "popup.html"
  "content_scripts": [
      "js": ["./content.js"],
      "matches": ["*://*"],
      "run_at": "document_end"

Service Workers (background.js)

These are files that you have to declare in manifest.json as they contain the logic that runs in the background of your application. It is common to name the main service worker file background.js. These files will be responsible for handling actions that do not happen inside the current page, such as what to do when a user clicks on the icon of your extension.

Images and other assets

These are files that are going to be available for your extension to use. They may include the icon of your extension or some image that you would like to show in your popup.


Not all extensions have a popup, but if yours does, you will need to declare the HTML file of your popup in your manifest.json. To enable a popup in your extension, write the location of your HTML file inside the action property of the manifest (like in the example above); otherwise, it will not show. Just by doing this, your popup will show whenever someone clicks on the icon of your extension.

Once that is done, you can add CSS and JS files to your extension and include them inside your popup like on a regular HTML page. Below is an example of how a popup.html file would look like. As you can see, we are adding a javascript file that is located in the same folder as the current HTML file.

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ChatGPT Extension</title>
  <link rel="stylesheet" href="popup.css" />
    <div id="app">
      <section class="header">
        <img src="./images/logo-popup.png" alt="" />
      <section class="buttons">
        <button id="btn-prompt">Send Prompt to ChatGPT</button>
    <script type="module" src="popup.js"></script>

As you can see, the popup behaves as its own front-end application. And, in some sense, that is what it is. This means that if you want to perform an action on the current tab when you click a button on the popup, you will have to send a request to the “tabs API” and then catch it in your open tab through the content.js file.

Content scripts (content.js)

These files will be loaded inside the web page that is open in the browser. They are declared in the content-scripts section of your manifest. If you want to alter the behavior of a web page with your extension, this is the file that you are going to need to update.

Also, if you want to perform a certain action on your page whenever someone clicks on an item that is not inside the page (a button in the popup or the icon of your extension, for example), this is where you will handle that request.

Sending data across tabs and popups

When you want to perform an action with the click of a button inside your web page, that is quite simple. All you have to do is add an EventListener to your button and plug a function that does what you want it to do.

But if that button is located in the popup of your extension, and you want to perform the action on your page, you cannot just do that directly. This is because the popup and your page are actually considered two different applications. You will thus have to send a request to the page via the “tabs API”. This allows Chrome users to make sure that whatever action an extension wants to perform on their open tabs is validated by Chrome beforehand.

Luckily enough, that API is relatively straightforward.

Send a message from your popup to your page

Let’s say that we want to change the background of the current website when the user clicks on a button in your popup. You will have to send a “request” to the chrome.tabs API… or, more precisely, a “message”. Messages are requests that you can send to other tabs and they can any data.

It is a common practice to give your message a property titled name or action so that it is easy to handle it on the receiving end.

For example, we will add this code inside our popup.js file (the one we added at the footer of our popup.html):

// popup.js

const myButton = document.querySelector("#my-button");

myButton.addEventListener("click", async () => {
    const response = await chrome.tabs.sendMessage(, { action: "CHANGE_BACKGROUND" });

This will send a “message” to the rest of the elements of your Chrome framework and you can catch it either in your background.js or in your content.js. In this case, we will catch it in our content script using the chrome.runtime.onMessage endpoint because we want the action to be performed inside our page.

// content.js

chrome.runtime.onMessage.addListener(async (request, sender, response) => {
      // console.log(request);

      if (request.action == "CHANGE_BACKGROUND") {
      if (request.action == "SAY_HELLO") {

As you can see, now you can send different requests depending on what the user does. Let’s say you want to add another button that sends a message of action SAY_HELLO. Then you can catch in on your content.js.

Why did we use content.js?

Why didn’t we catch that message on our service worker, background.js? That’s because only content scripts have access to the open page. Service workers run in the background and can handle requests if you want to do something else, such as requesting data from a remote API or just handling what happens when you click on the button.

A note on permissions

Remember to add which pages you want your extension to work on. Because your content.js will not load by default into every single page the user opens (that could be delicate, privacy-wise). So be sure to detail on which hosts your extension is allowed to load custom javascript. This is what you have to write under the content-scripts.matches property of your manifest. For example, if you want your content.js to be loaded only to ChatGPT, you should add the following to your manifest:

"content_scripts": [
      "js": ["./content.js"],
      "matches": ["*://*"],
      "run_at": "document_end"


That’s all! Now you know how to create a Chrome extension using the new Manifest V3. I hope this was useful and that you will build many different cool things.