I made a Chrome extension this weekend because I found I was doing the same task over and over and wanted to automate it. Plus, I’m a nerd living through a pandemic, so I spend my pent-up energy building things. I’ve made a few Chrome Extensions over the years, hope this post helps you get going, too. Let’s get started!
Create the manifest
The first step is creating a manifest.json
file in a project folder. This serves a similar purpose to a package.json
, it provides the Chrome Web Store with critical information about the project, including the name, version, the required permissions, and so forth. Here’s an example:
{
"manifest_version": 2,
"name": "Sample Name",
"version": "1.0.0",
"description": "This is a sample description",
"short_name": "Short Sample Name",
"permissions": ["activeTab", "declarativeContent", "storage", "<all_urls>"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["background.css"],
"js": ["background.js"]
}
],
"browser_action": {
"default_title": "Does a thing when you do a thing",
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png"
}
}
}
You might notice a few things- first: the names and descriptions can be anything you’d like.
The permissions depend on what the extension needs to do. We have ["activeTab", "declarativeContent", "storage", "<all_urls>"]
in this example because this particular extension needs information about the active tab, needs to change the page content, needs to access localStorage
, and needs to be active on all sites. If it only needs it to be active on one site at a time, we can remove the last index of that array.
A list of all of the permissions and what they mean can be found in Chrome’s extension docs.
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["background.css"],
"js": ["background.js"]
}
],
The content_scripts
section sets the sites where the extension should be active. If you want a single site, like Twitter for example, you would say ["https://twitter.com/*"]
. The CSS and JavaScript files are everything needed for extensions. For instance, my productive Twitter extension uses these files to override Twitter’s default appearance.
"browser_action": {
"default_title": "Does a thing when you do a thing",
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png"
}
}
There are things in browser_action
that are also optional. For example, if the extension doesn’t need a popup for its functionality, then both the default_title
and default_popup
can be removed. In that case, all that’s needed the icon for the extension. If the extension only works on some sites, then Chrome will grey out the icon when it’s inactive.
Debugging
Once the manifest, CSS and JavaScript files are ready, head over to chrome://extensions/
from the browser’s address bar and enable developer mode. That activates the “Load unpacked” button to add the extension files. It’s also possible to toggle whether or not the developer version of the extension is active.

I would highly recommend starting a GitHub repository to version control the files at this point. It’s a good way to save the work.
The extension needs to be reloaded from this interface when it is updated. A little refresh icon will display on the screen. Also, if the extension has any errors during development, it will show an error button with a stack trace and more info here as well.
Popup functionality
If the extension need to make use of a popup that comes off the extension icon, it’s thankfully fairly straightforward. After designating the name of the file with browser_action
in the manifest file, a page can be built with whatever HTML and CSS you’ll like to include, including images (I tend to use inline SVG).
We’ll probably want to add some functionality to a popup. That make take some JavaScript, so make sure the JavaScript file is designated in the manifest file and is linked up in your popup file as well, like this: <script src="background.js"></script>
In that file, start by creating functionality and we’ll have access to the popup DOM like this:
document.addEventListener("DOMContentLoaded", () => {
var button = document.getElementById("submit")
button.addEventListener("click", (e) => {
console.log(e)
})
})
If we create a button in the popup.html
file, assign it an ID called submit
, and then return a console log, you might notice that nothing is actually logged in the console. That’s because we’re in a different context, meaning we’ll need to right-click on the popup and open up a different set of DevTools.

We now have access to logging and debugging! Keep in mind, though, that if anything is set in localStorage
, then it will only exist in the extension’s DevTools localStorage
; not the user’s browser localStorage
. (This bit me the first time I tried it!)
Running scripts outside the extension
This is all fine and good, but say we want to run a script that has access to information on the current tab? Here’s a couple of ways we would do this. I would typically call a separate function from inside the DOMContentLoaded
event listener:
Example 1: Activate a file
function exampleFunction() {
chrome.tabs.executeScript(() => {
chrome.tabs.executeScript({ file: "content.js" })
})
}
Example 2: Execute just a bit of code
This way is great if there’s only a small bit of code to run. However, it quickly gets tough to work with since it requires passing everything as a string or template literal.
function exampleFunction() {
chrome.tabs.executeScript({
code: `console.log(‘hi there’)`
})
}
Example 3: Activate a file and pass a parameter
Remember, the extension and tab are operating in different contexts. That makes passing parameters between them a not-so-trivial task. What we’ll do here is nest the first two examples to pass a bit of code into the second file. I will store everything I need in a single option, but we’ll have to stringify the object for that to work properly.
function exampleFunction(options) {
chrome.tabs.executeScript(
{ code: "var options = " + JSON.stringify(options) },
function() {
chrome.tabs.executeScript({ file: "content.js" })
}
)
}
Icons
Even though the manifest file only defines two icons, we need two more to officially submit the extension to the Chrome Web Store: one that’s 128px square, and one that I call icon128_proper.png
, which is also 128px, but has a little padding inside it between the edge of the image and the icon.
Keep in mind that whatever icon is used needs to look good both in light mode and dark mode for the browser. I usually find my icons on the Noun Project.
Submitting to the Chrome Web Store
Now we get to head over to the Chrome Web Store developer console to submit the extension! Click the “New Item” button, the drag and drop the zipped project file into the uploader.

From there, Chrome will ask a few questions about the extension, request information about the permissions requested in the extension and why they’re needed. Fair warning: requesting “activeTab”
or “tabs”
permissions will require a longer review to make sure the code isn’t doing anything abusive.
That’s it! This should get you all set up and on your way to building a Chrome browser extension! 🎉
i use extensionizr.com
This is cool!
Thank you! I recently developed an extension as well and I had to browse through super old tutorials and docs to get exactly these kinda informations together. Just bookmarked this. <3
Never thought about automating things via browser extension so thank you for both the idea and the guide.
I was able to build an extension parsing a JSON response from our API which is also something I’ve never done before.
You mentioned background.js in content scripts section in the manifest,there is also a context called background scripts in extension runtime, make sure you never confuse between these two.
Hey Sarah, great stuff! Thanks for writing this. There’s not enough good content about writing extensions and the docs are not so amazing and sometimes even confusing unfortunately.
What’s the purpose of using
executeScript
here? Your content scripts should already have access to the DOM by default as they’re loaded just like any other script tag on pages where the URL matches the sites you listed in the manifest.I agree with the person above me that it’s a bit confusing to name your content script file
background.js
as there’s a big difference between the role of content scripts that run on the page as I mentioned and background scripts that are declared separately in the manifest and are always running on a thread in the background as long as your extension is enabled but they do not have access to the tabs’ content (used for things like listening to post messages, maintaining state across tabs/pages, handling post install/update events, etc..I hope this helps and thanks again, I love your content and appreciate that you’re always taking the time to write and educate!
Are you really upload your extension with chrome.tabs.executeScript? And Chrome Web Store approved it? I doubt.
chrome.tabs.executeScript is a serious security vulnerability. I think CWS will block such extension.
If you want to run script in the current tab page context you should call sendMessage to “content script” and then to handle that message there.
It’s approved. There’s also a more constructive way to share knowledge than this.
This was super helpful! Just finished submitting my first extension.
Hello Sarah, thanks a lot for the great content. I have been following your post to create my extension but when I tried to inject a js file the way you do I’m getting this error:
“Uncaught TypeError: Error in invocation of tabs.executeScript(optional integer tabId, extensionTypes.InjectDetails details, optional function callback): No matching signature.”
I’m basically using the same approach that you’re using to add the js file but apparently there’s a problm with the “executeScript” statement:
$(document).ready(() => {
var button = document.getElementById(“submit”);
function exampleFunction() {
chrome.tabs.executeScript(() => {
chrome.tabs.executeScript({ file: “content.js” });
});
}
$(button).click((e) => {
exampleFunction();
});
});
Glad I found this! I’m not entirely sure what it is, but I always seem to struggle with the manifest file and assigning the right permissions, so thanks for the clarification!
Out of curiosity, what is your opinion on something like Extensionizr.com or ChromeExtensionKit.com to help a with a lot of the setup?
I think a really good example is GoodPlan Notes from the store from technical point of view.
Setting content_scripts does not disable extension on sites that don’t match the matches array.
One way I found to make it work is
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [new chrome.declarativeContent.PageStateMatcher({
pageUrl: {hostEquals: ‘domain.com’},
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
question:
are we super sure we need nested
executeScript
in the example?Guys, can we have a new tutorial for Manifest v3 please? Banging my head against the wall currently, with such few Manifest v3 resources available right now.