DEV Community

Cover image for How to create your first Chrome extension
Ayooluwa Isaiah
Ayooluwa Isaiah

Posted on • Originally published at freshman.tech on

How to create your first Chrome extension

I originally posted this on my blog a month ago. If it's interesting to you, I post new tutorials on freshman.tech couple of times a month.

Chrome extensions are programs that enhance the functions of the browser in some way. You probably already use an extension or two, maybe a password manager or some privacy addons. Have you ever wanted to create your own extensions but wondered how difficult the process would be? With the help of this tutorial, you will get a first-hand experience of just how easy it can be.

This article will walk you through the process of creating your first Chrome extension. We'll build an extension that replaces the new tab page in the browser with a random photo from Unsplash. It's a miniature version of my Stellar Photos extension which I built a few years ago when I first learned to build Chrome extensions. Here's a demonstration of how the finished extension will look like:

I've also included some tips for debugging Chrome extensions as well as links to resources where you can learn how to submit your extension to the Chrome web store. This will come in handy later on when you're making your own extensions. The complete code for this demo project can be found here.

Prerequisites

You need to have a basic knowledge of HTML, CSS, JavaScript and the command-line to follow through with this tutorial. You also need the latest version of Chrome installed on your computer. I tested the code used for this project on Chrome 85 but it should keep working on any later version.

Grab the starter files

The starter files for this tutorial are on GitHub. The repository includes all the markup and styles for the extension we’ll be building. You can run the command below in your terminal to clone the repository to your filesystem or download the zip file and extract it on your computer.

$ git clone https://github.com/Freshman-tech/freshtab-starter-files.git
Enter fullscreen mode Exit fullscreen mode

Once the repository has been downloaded, cd into it in your terminal and use the tree command (if you have it installed on your computer) to inspect the directory structure.

$ cd freshtab-starter-files
$ tree
.
├── css
│   └── styles.css
├── demo.jpg
├── icons
│   ├── 128.png
│   ├── 16.png
│   ├── 32.png
│   ├── 48.png
│   └── 64.png
├── index.html
├── js
│   ├── background.js
│   ├── index.js
│   └── popup.js
├── LICENCE
├── manifest.json
├── popup.html
└── README.md
Enter fullscreen mode Exit fullscreen mode

If you don't have the tree command, you can navigate to the directory in your file manager and inspect its contents that way.

Anatomy of a Chrome extension

Chrome extensions are composed of different files depending on the nature of the extension. Usually, you'll see a manifest file, some icons, and several HTML, CSS, and JavaScript files which compose the different interfaces of the extension. Let's take a quick look at the files contained in the project directory to see what they all do.

The manifest

This file (manifest.json) defines the structure of the extension, the permissions it needs, and other details such as name, icons, description, supported browser versions, e.t.c.

Background scripts

Background scripts are those that run in the background, listening for events and reacting to messages sent from other scripts that make up the extension. These scripts are defined in the manifest file. Our project has just one background script: the aptly named background.js file in the js folder.

Popup window

A popup is the small window displayed when a user clicks the toolbar icon in the browser interface. It is an HTML file that can include other resources such as stylesheets and scripts, but inline scripts are not allowed.

Lastpass Chrome extension popup

To use a popup in your extension, you need to define it in the manifest first. The popup file for this extension is popup.html which links to the popup.js in the js folder.

Override pages

Extensions can override browser pages such as the new tab page, history or bookmarks but only one at a time. All you need to do is specify an HTML file in the manifest and the page to be replaced (newtab, bookmarks, or history). In this case, the index.html file will override the new tab page.

Extension icons

It's necessary to include at least one icon in the extension manifest to represent it otherwise a generic one will be used instead. The icons for our extension are in the icons directory.

Content scripts

Content scripts are those that will be executed in web pages loaded in your browser. They have full access to the DOM and can communicate with other parts of the extension through the messaging API. We don't need a content script for this particular project, but extensions that need to modify the DOM of other web pages do.

Update the manifest file

Let's start building the Chrome extension by defining the required fields in the manifest.json file. Open up this file in your text editor and update it with the following code:

{
  "manifest_version": 2,
  "name": "freshtab",
  "version": "1.0.0",
  "description": "Experience a beautiful photo from Unsplash every time you open a new tab.",
  "icons": {
    "16": "icons/16.png",
    "32": "icons/32.png",
    "48": "icons/48.png",
    "64": "icons/64.png",
    "128": "icons/128.png"
  },
  "chrome_url_overrides": {
    "newtab": "index.html"
  },
  "browser_action": {
    "default_popup": "popup.html"
  },
  "permissions": ["storage", "unlimitedStorage"],
  "background": {
    "scripts": ["js/background.js"],
    "persistent": false
  },
  "minimum_chrome_version": "60"
}
Enter fullscreen mode Exit fullscreen mode

Here's a breakdown of each field in the manifest file:

Required fields

  • manifest_version: this key specifies the version of the manifest.json used by this extension. Currently, this must always be 2.
  • name: the extension name.
  • version: the extension version.

Optional but recommended fields

  • description: the extension description.
  • icons: this specifies icons for your extension in different sizes.

Optional

  • chrome_url_overrides: used to provide a custom replacement for browser pages. In this case,the new tab page is being replaced with the index.html file.
  • browser_action: used to define settings for the button that the extension adds to the browser toolbar, including a popup file if any.
  • permissions: used to define the permissions required by the extension. We need the storage permission to access the Chrome storage API, and unlimitedStorage to get an unlimited quota for storing client side data (instead of the default 5MB).
  • background: used to register background scripts. Setting the persistent key to false keeps the script from being retained in memory when not in use.
  • minimum_chrome_version: The minimum version required by your extension. Users on Chrome versions earlier than the specified one will be unable to install the extension.

Load the extension in Chrome

Open up your Chrome browser and enter chrome://extensions in the address bar. Ensure Developer mode is enabled, then click the Load unpacked button and select the extension directory. Once the extension is loaded, it will appear in the first position on the page.

Alt Text

At this point, the browser's new tab page will be replaced by the one defined in our extension manifest (index.html). Try it out by opening a new tab. You should see a blank page as shown in the screenshot below:

Alt Text

Get your Unsplash access key

Before you can use the Unsplash API, you need to create a free
account on their website first. Follow the instructions on this
page
to do so, and register a new application.
Once your app is created, take note of the access key string in the application settings page.

Alt Text

Fetch the background image

The first step is to fetch a random image from Unsplash. An API endpoint exists for this purpose:

https://api.unsplash.com/photos/random
Enter fullscreen mode Exit fullscreen mode

This endpoint accepts a number of query parameters for the purpose of narrowing the pool of photos from which a random one will be chosen. For example, we can use the orientation parameter to limit the results to landscape images only.

https://api.unsplash.com/photos/random?orientation=landscape
Enter fullscreen mode Exit fullscreen mode

Let's use the fetch API to retrieve a single random photo from Unsplash. Add the following code to your js/background.js file:

// Replace <your unsplash access key> with the Access Key retrieved
// in the previous step.
const UNSPLASH_ACCESS_KEY = '<your unsplash access key>';

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }

  return response;
}

async function getRandomPhoto() {
  const endpoint = 'https://api.unsplash.com/photos/random?orientation=landscape';

  // Creates a new Headers object.
  const headers = new Headers();
  // Set the HTTP Authorization header
  headers.append('Authorization', `Client-ID ${UNSPLASH_ACCESS_KEY}`);

  let response = await fetch(endpoint, { headers });
  const json = await validateResponse(response).json();

  return json;
}

async function nextImage() {
  try {
    const image = await getRandomPhoto();
    console.log(image);
  } catch (err) {
    console.log(err);
  }
}

// Execute the `nextImage` function when the extension is installed
chrome.runtime.onInstalled.addListener(nextImage);
Enter fullscreen mode Exit fullscreen mode

The /photos/random endpoint requires authentication via
the HTTP Authorization header. This is done by setting the Authorization header to Client-ID ACCESS_KEY where ACCESS_KEY is your application's Access Key. Without this header, the request will result in a 401 Unauthorized response.

Once this request is made and a response is received, the validateResponse() function is executed to check if the response has a status code of 200 OK. If so, the response is read as JSON, and automatically wraps it in a resolved promise. Otherwise, an error is thrown and getRandomPhoto() photo rejects with an error.

You can try this out by reloading the extension on the chrome://extensions page after saving the file, then click the background page link to inspect the console for the script.

Alt Text

Note: Make sure you always reload the extension after making a change to the background.js file so that the new changes are effected.

You should see the JSON object received from Unsplash in the console. This object contains a lot of info about the image including its width and height, number of downloads, photographer information, download links, e.t.c.

We need to save this object in the Chrome storage and use it to set the background image whenever a new tab is opened. Let's tackle that in the next step.

Alt Text

Save the image object locally

We cannot set the background image on our new tab pages directly from the background.js, but we need a way to access the Unsplash object from the new tab pages.

One way to share data between a background script and the other scripts that make up the extension is to save the data to a location which is accessible to all the scripts in the extension. We can use the browser localStorage API or chrome.storage which is specific to Chrome extensions. We'll opt for the latter in this tutorial.

Modify the nextImage() function in your background.js file as shown below:

async function nextImage() {
  try {
    const image = await getRandomPhoto();
    // Save the `image` object to chrome's local storage area
    // under the `nextImage` key
    chrome.storage.local.set({ nextImage: image });
  } catch (err) {
    console.log(err);
  }
}
Enter fullscreen mode Exit fullscreen mode

To store data for your extension, you can use either chrome.storage.sync or chrome.storage.local. The former should be used if you want the data to be synced any Chrome browser that the user is logged into, provided the user has sync enabled. We don't need to sync the data here so the latter option is more appropriate here.

Set the background image on each new tab page

Now that the Unsplash object is being saved to the extension's local storage, we can access it from the new tab page. Update your js/index.js file as shown below:

function setImage(image) {
  document.body.setAttribute(
    'style',
    `background-image: url(${image.urls.full});`
  );
}

document.addEventListener('DOMContentLoaded', () => {
  // Retrieve the next image object
  chrome.storage.local.get('nextImage', data => {
    if (data.nextImage) {
      setImage(data.nextImage);
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Once the DOM is loaded and parsed, the data stored in the nextImage key is retrieved from the Chrome's local storage compartment for extensions. If this data exists, the setImage() function is executed with the nextImage object as it's sole argument. This function sets the background-image style on the <body> element to the URL in the image.urls.full property.

Alt Text

At this point, opening a new tab will load a background image on the screen but the image loads slowly at first because it is being fetch freshly from the server when the tab is opened. This problem can be solved by saving the image data itself to the local storage instead of a link to the image. This will cause the background image to load instantly when a new tab is opened, because it will
be fetched from the local storage not the Unsplash servers. To save image data to local storage, we need to encode it to
Base64 format. For example, here's the Base64 encoding of this image:

Encoding an image into the Base64 format produces a string that represents entire image data. You can test this by pasting the Base64 string in your browser's address bar. It should load the image represented by the string as demonstrated below:

Alt Text

What we need to do next is convert each image received from the Unsplash API to a Base64 string and attach it to the image object before storing it in the local storage. Once a new tab is opened, the Base64 string will be retrieved and used in the background-image property instead of the image URL.

To convert an image to a Base64 string, we need to retrieve the image data first. Here's how:

async function getRandomPhoto() {
  let endpoint = 'https://api.unsplash.com/photos/random?orientation=landscape';

  const headers = new Headers();
  headers.append('Authorization', `Client-ID ${UNSPLASH_ACCESS_KEY}`);

  let response = await fetch(endpoint, { headers });
  const json = await validateResponse(response).json();
  // Fetch the raw image data. The query parameters are used to control the size
  // and quality of the image:
  // q - compression quality
  // w - image width
  // See all the suported parameters: https://unsplash.com/documentation#supported-parameters
  response = await fetch(json.urls.raw + '&q=85&w=2000');
  // Verify the status of the response (must be 200 OK)
  // and read a Blob object out of the response.
  // This object is used to represent binary data and
  // is stored in a new `blob` property on the `json` object.
  json.blob = await validateResponse(response).blob();

  return json;
}
Enter fullscreen mode Exit fullscreen mode

The raw URL consists of a base image URL which we can add additional image
parameters
to control the size, quality and format of the image. The query parameters &q=85&w=2000 will produce an image with a width of 2000px and 85% quality compared to the original. This should represent a good enough quality for most screen sizes.

To read the image data from the response, the blob() method is used. This returns a Blob object that represents the image data. This object is then set on a new blob property on the json object. The next step is to encode the blob object into a Base64 string so that it may be saved to local storage. Modify the nextImage()
function in your background.js file as shown below:

async function nextImage() {
  try {
    const image = await getRandomPhoto();

    // the FileReader object lets you read the contents of
    // files or raw data buffers. A blob object is a data buffer
    const fileReader = new FileReader();
    // The readAsDataURL method is used to read
    // the contents of the specified blob object
    // Once finished, the binary data is converted to
    // a Base64 string
    fileReader.readAsDataURL(image.blob);
    // The `load` event is fired when a read
    // has completed successfully. The result
    // can be found in `event.target.result`
    fileReader.addEventListener('load', event => {
      // The `result` property is the Base64 string
      const { result } = event.target;
      // This string is stored on a `base64` property
      // in the image object
      image.base64 = result;
      // The image object is subsequently stored in
      // the browser's local storage as before
      chrome.storage.local.set({ nextImage: image });
    });
  } catch (err) {
    console.log(err);
  }
}
Enter fullscreen mode Exit fullscreen mode

The FileReader API is how we convert the image blob to a Base64 encoded string. The readAsDataURL() method reads the contents of the image.blob property. When the read is completed, the load event is fired and the result of the operation can be accessed under event.target.result as shown above. This result property is a Base64 encoded string which is then stored on the image object in a new base64 property and the entire object is
subsequently stored in Chrome's local storage area for extensions.

Note: Make sure to reload the extension after saving your background.js file

The next step is to update the value of the background style used to set the body background in setImage function. Replace image.urls.full with image.base64 as shown below:

function setImage(image) {
  document.body.setAttribute(
    'style',
    `background-image: url(${image.base64});`
  );
}
Enter fullscreen mode Exit fullscreen mode

If you open a new tab, you will observe that the background image loads instantly. This is because the image is being retrieved from the local storage in its Base64 string form instead of being freshly loaded from Unsplash servers like we were doing earlier.

Alt Text

Load new images on each tab

At the moment, the nextImage() function is invoked only when the extension is first installed or reloaded. This means that the only way to cause a new image to load is to reload the extension in the extensions page. In this section, we'll figure out a way to invoke nextImage() each time a new tab is opened so that a new image is fetched in the background to replace the previous one without having to reload the extension each time.

// This line is what causes the nextImage() function to be
// executed when the extension is freshly installed or reloaded.
chrome.runtime.onInstalled.addListener(nextImage);
Enter fullscreen mode Exit fullscreen mode

The background.js script is not aware of when a new tab is open, but this index.js script is because it is a part of the custom new tab page. To communicate between the two scripts we need to send a message from one script and listen for the message in another script.

We will use the chrome.runtime.sendMessage and chrome.runtime.onMessage functions to add communication between the background script and new tab script. The former will be used in our index.js file to notify the background script that a new image should be fetched in the background. Modify your index.js file as shown below:

document.addEventListener('DOMContentLoaded', () => {
  chrome.storage.local.get('nextImage', (data) => {
    if (data.nextImage) {
      setImage(data.nextImage);
    }
  });

  // Add the line below
  chrome.runtime.sendMessage({ command: 'next-image' });
});
Enter fullscreen mode Exit fullscreen mode

Each time a new tab page loads, a message will be sent with the message object shown above. This message object can be any valid JSON object. You can also add an optional callback function as a second argument to sendMessage() if you need to handle a response from the other end but we don't need that here.

The next step is to use the chrome.runtime.onMessage method in our background script to listen for message events and react appropriately when triggered. Add the code to the bottom of your background.js file:

chrome.runtime.onInstalled.addListener(nextImage);

// Add the lines below
chrome.runtime.onMessage.addListener((request) => {
  if (request.command === 'next-image') {
    nextImage();
  }
});
Enter fullscreen mode Exit fullscreen mode

The onMessage function is used to register a listener that listens for messages sent by chrome.runtime.sendMessage. The addListener method takes a single callback function which can take up to three parameters:

  • request: The message object from the sender
  • sender: The sender of the request
  • sendResponse: A function to call if you want to respond to the sender

We are not using sender or sendResponse in this case so I've left it out of the callback function. In the body of the function, an if statement is used to check the message object. If it corresponds to the message object from the new tab script, the nextImage() function is executed, causing a new image to replace the previous one.

Reload the extension and open a few new tab pages. You should see a new background image in the tabs each time. If you see the same image multiple times, it could be due to any of the two reasons below:

  • The next image is still loading in the background. The speed at which a new image can be retrieved and saved is mostly limited by your internet connection.
  • The same image is returned consecutively from Unsplash. Since images are fetched at random, there is no guarantee that a different image will be received each time. However, the pool of images from which a random one is selected is so large (except you restrict it to specific Unsplash collections) that this is unlikely to happen often.

Restrict images to user defined collections

At the moment, the pool of images from which a random one is selected is only limited by orientation according to the value of the endpoint variable in getRandomPhoto():

https://api.unsplash.com/photos/random?orientation=landscape
Enter fullscreen mode Exit fullscreen mode

We can use any of the other available query parameters to further limit the pool of images. For example, we can filter images by collection:

https://api.unsplash.com/photos/random?orientation=landscape&collection=998309,317099
Enter fullscreen mode Exit fullscreen mode

You can retrieve a collection ID by heading to the collections page and selecting the ID from any collection URL as shown below:

Alt Text

Let's add the ability for a user to optionally restrict the pool of images to those from a specific collection. We'll create a way to do that through thepopup window which is a common way through with basic extension settings are configured. Here's how the popup window is setup at the moment:

Alt Text

If you don't see the extension icon in the top bar, make sure the icon is pinned as demonstrated in the screenshot below:

Alt Text

The popup window has a single input where a user may enter one or more collection IDs. The markup for this window is in the popup.html file if you want to inspect it. Our first task is to validate and save any custom collection IDs to the local storage. Open up the js/popup.js file in your text editor, and populate its contents with the following code:

const input = document.getElementById('js-collections');
const form = document.getElementById('js-form');
const message = document.getElementById('js-message');

const UNSPLASH_ACCESS_KEY = '<your unsplash access key>';

async function saveCollections(event) {
  event.preventDefault();
  const value = input.value.trim();
  if (!value) return;

  try {
    // split the string into an array of collection IDs
    const collections = value.split(',');
    for (let i = 0; i < collections.length; i++) {
      const result = Number.parseInt(collections[i], 10);
      // Ensure each collection ID is a number
      if (Number.isNaN(result)) {
        throw Error(`${collections[i]} is not a number`);
      }

      message.textContent = 'Loading...';
      const headers = new Headers();
      headers.append('Authorization', `Client-ID ${UNSPLASH_ACCESS_KEY}`);

      // Verify that the collection exists
      const response = await fetch(
        `https://api.unsplash.com/collections/${result}`,
        { headers }
      );

      if (!response.ok) {
        throw Error(`Collection not found: ${result}`);
      }
    }

    // Save the collecion to local storage
    chrome.storage.local.set(
      {
        collections: value,
      },
      () => {
        message.setAttribute('class', 'success');
        message.textContent = 'Collections saved successfully!';
      }
    );
  } catch (err) {
    message.setAttribute('class', 'error');
    message.textContent = err;
  }
}

form.addEventListener('submit', saveCollections);

document.addEventListener('DOMContentLoaded', () => {
  // Retrieve collecion IDs from the local storage (if present)
  // and set them as the value of the input
  chrome.storage.local.get('collections', (result) => {
    const collections = result.collections || '';
    input.value = collections;
  });
});
Enter fullscreen mode Exit fullscreen mode

Although it's a sizeable chunk of code, it's nothing you haven't seen before. When the Enter key is pressed on the input, the form is submitted and saveCollections() is executed. In this function, the collection IDs are processed and eventually saved to chrome's local storage for extensions. Don't forget to replace the <your unsplash access key> placeholder with your actual access key.

The next step is to use any saved collection IDs in the request for a random image. Open up your background.js file and update it as shown below:

function getCollections() {
  return new Promise((resolve) => {
    chrome.storage.local.get('collections', (result) => {
      const collections = result.collections || '';
      resolve(collections);
    });
  });
}

async function getRandomPhoto() {
  const collections = await getCollections();

  let endpoint = 'https://api.unsplash.com/photos/random?orientation=landscape';

  if (collections) {
    endpoint += `&collections=${collections}`;
  }

  const headers = new Headers();
  headers.append('Authorization', `Client-ID ${UNSPLASH_ACCESS_KEY}`);

  let response = await fetch(endpoint, { headers });
  const json = await validateResponse(response).json();
  response = await fetch(json.urls.raw + '&q=85&w=2000');
  json.blob = await validateResponse(response).blob();

  return json;
}
Enter fullscreen mode Exit fullscreen mode

The getCollections() function retrieves any saved collection IDs. If any one has been specified by the user, it is appended to the endpoint via the &collections query parameter. That way, the random image will be fetched from the specified collections instead of the entire Unsplash catalogue.

Tips for debugging

Chrome extensions use the same debugging workflow as regular web pages, but they have some unique properties you need to be aware of. To debug your background script, head to the chrome extensions page at chrome://extensions and ensure Developer mode is enabled. Next, find your extension and click background page under inspect views. This will open a DevTools window for debugging purposes.

Alt Text

Debugging a popup window can be done by right clicking on the popup icon and then clicking Inspect popup. This will launch a DevTools window for your popup. For the new tab page (or other override pages), debug them like you would do a regular webapage (using Ctrl+Shift+I to launch the DevTools panel).

Alt Text

During development, you may see an Errors button next to Details and Remove on your extension entry. This indicates that an error occurred somewhere in your extension code. Click this button to find out the exact line in your code where the error occurred.

Alt Text

Publishing your extension

Follow the steps detailed in this guide to publish your extension to the Chrome web store. A Google account is required.

Conclusion

Congratulations, you've successfully built your first Chrome extension. I hope you had fun building it! Feel free to leave a comment below if you have any questions or suggestions. If you want to see a more fully-fledged implementation of this particular type of Chrome extension, checkout Stellar Photos on GitHub.

Thanks for reading, and happy coding!

Top comments (1)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

Why not mention that this will also (probably) work on Firefox and other browsers?