One of the best advantages of being a developer is that you can create your own set of tools to optimize your workflow. Creating chrome extensions is no exception from this.
We all use some from our day to day basis — probably most of you reading this article with an adblocker installed — but some of us have special workflows for which we don’t have appropriate tools.
Creating them is not much hassle; You make it once, use it forever. The amount of time they can save is invaluable so I decided to give it a go and write a tutorial about it.
What will be creating?
I’ve created some simple designs in Figma. The extension can be used to import ingredients from Tasty, which you can later use in your shopping list. I chose this example so we can see how to implement a popup with various UI elements with logic behind it. We will also have a look at how we can interact with the current page to do DOM manipulation through the use of content scripts. Last but not least, we will see how we can store information inside our extension for later use.
The extension will work in the following way: first we want to check if the user is on the correct website. In case they are not, we display a message to them to visit a recipe in order to start importing. If they are on the correct site, we present the user with an import button that will fetch the ingredients from the page and creates a checklist.
To get started, create an empty directory which will hold all the required files for the extension. All extensions need to have a
manifest.json file. This is a configuration file that will hold information about our extension, such as the file used for the popup window, the scripts we are using for it or even the permissions we want to request for the extension.
We can get away using only four properties initially. At this stage, we can already import the extension to Chrome so we can check any further updates. To do so, navigate to
chrome://extensions and enable developer mode then click on the “load unpacked” button and select the folder of the extension to import it.
The extension is now installed and you should see a new icon appearing next to the address bar. Right at this stage it looks pretty boring. We have the name, the version and the description displayed, but we don’t have an icon, so let’s add that right now.
Create four different sized png icons:
128x128px. You can do it with the help of online tools or with your favourite design tool, I use Figma throughout this tutorial.
imgs folder for them and pull it into your project, so now you have a folder structure similar to the following:
To use the images in the extension, we can expand the
manifest.json file with an
icons property, holding the references to them:
If you go back to the extensions tab and refresh the extension, you’ll see we have an icon now.
Adding the popup
With everything set up, let’s add the user interface to the extension. Create a
popup.html file in the root of your project. Just as for the images, we need to reference it in the
This can be done by adding the
browser_action property. The
popup.html holds all of the UI elements displayed in the extension. Right at this stage it only includes a generic message for all sites:
This is all the HTML we will need. For the styles, I’ve created a
css folder and put all my rules into the
Showing the import button
Right now, even if you are on Tasty.co, the same message is presented to the user. We want to hide the disclaimer and show the import button if the user is viewing a recipe.
To do so, we need to add a script to our
popup.html file. Create an
app.js in your root directory and reference it inside
popup.html, right before the closing of the body tag.
We’re going to be using the
chrome.tabs API. To have access to everything, we need to request a permission for it which we can do in our
manifest.json file. Expand it with the following line:
Now we can start writing out the functionality for showing the import button to the user:
We use the
getSelected method of
chrome.tabs API which returns the currently active tab. If the tab URL matches the path we defined on line:2, we hide the disclaimer and show the shopping list to the user.
After you’ve made the changes, remember to refresh the extension to see the results.
Pulling in ingredients with content scripts
By clicking the button, we would like to get the list of ingredients from the page and populate the
ingredients node inside our extension. As you can see from the code example above, the
document node represents the HTML of the extension and not the document of the page. To get around this, we will use content scripts.
Content scripts have access to the page’s DOM, but have no access to the extension’s HTML.
Extensions in Chrome are event based, so to make things work when the user clicks the import button, we need to:
- Send a message to the content script telling it to fetch the ingredients
- Get the ingredients from the DOM inside the content script
- Send the response back to our
- Populate the shopping list with the retrieved data
It may sound overly complicated at first but bear with me, each step can be done with only a couple of lines.
To add a content script, navigate back to your
manifest.json and extend it with the
This is how our
manifest.json file looks like. The
content_scripts property takes in an array of objects with two properties:
matchestells the extension where to inject the script into the page
jstells the extension which file to inject into the page
matches property we can use regexes. With
* we tell it to match for every page that starts with
tasty.co/recipe/. Inside the
js property, we are referencing
contentScript.js so create that in your root directory.
If everything done correctly, you should see your content script loaded only for recipe pages inside Tasty. To debug content scripts, inspect the page and go to your “Sources” tab, then change “Page” on the left hand side to “Content scripts”. There you will see your extension.
contentScript.js we are going to add a listener for the event when the user clicks the import button:
To add the event listener we use
chrome.runtime.onMessage.addListener. Inside its callback, we check if the request is coming from our app and if it does, we send back the name of the recipe alongside with the ingredients in an object.
To send this event, we need to go back to our
app.js file and add the event listener to our button.
Here we use
chrome.tabs.sendMessage with the id of the current tab and the same action we are listening for inside
contentScript.js. After extending
app.js with the lines above, clicking on the import button should grant you the information we are longing for.
All that’s left to do for us is populating the DOM inside the extension with the information we got from our content script.
Populating the extension with the ingredients
chrome.tabs.sendMessage when the response comes back, we want to populate the extension with the title and the ingredients.
Starting with the title we can get it done using one single line:
Doing the ingredients will be a little bit more tricky but nothing impossible. Since we have multiple elements, we need to loop over them. We will also have checkboxes inside the list item so we need to create additional DOM elements as well.
Here we first get the
.ingredients list, we want query it only once before the loop. Next we loop over
forEach. Inside each ingredient, we create the
li and the
input, we set the
type and append the checkbox to the list item and the name of the ingredient after. Then we append the whole thing to the
Last but not least, we can hide the import button to prevent the user to keep importing the same list again and again.
Now if we refresh the extension and test it out on a site, you should see the ingredients getting pulled in, but if we try to check one of the items, nothing happens. That’s because we haven’t attached any listener to the checkboxes yet.
Checking the checkboxes
To add the functionality to the checkboxes, lets create an event listener next to the import buttons event listener. Now we can’t just attach listeners to the
li elements, as initially they don’t exist in the DOM. Instead, we can attach an event listener to the parent container which is the
To check whether the input has been clicked inside the list item we check for
e.target.nodeName. Giving the items a strike through is just a matter of switching a class based on whether the checkbox is checked or not.
Storing previously fetched data
Of course when we close the extension and reopen it later, we need to import the same recipes again as everything is lost. To get around this, let’s add some functionality to store previously imported data.
For that, we’re going to use the
storage API, so we need to request a permission for it in our
To store the response returned from the content script, we can call
storage.sync.set after we clicked the import button and the response is returned:
To retrieve the response, we can call
storage.sync.get right after we check if we are or Tasty.co. Since we would populate the same way we do for the import button, we can extract the code into a function and call it for both importing and
We can also introduce a clear button that calls
storage.sync.clear to get rid of previously stored data.
This is how our whole
app.js file will look in the end, with everything in place:
We looked at how we can initialize a Chrome extension with a manifest file, how we can request permissions to various APIs such as
storage, and we also looked at creating a popup from scratch and using content scripts to retrieve data from a website.
This is just the tip of iceberg. If you would like to learn more, the official Chrome documentation is pretty extensive, you can find different pages for the different APIs, I would like to encourage you to take a look and experiment with different setups and try to extend the extension created in this tutorial with various funcionality. 🌶️