MDN wants to learn about developers like you: https://qsurvey.mozilla.com/s3/MDN-dev-survey

If you've been through the Your first extension article, you've already got an idea of how to write a extension. In this article we'll write a slightly more complex extension that demonstrates a few more of the APIs.

The extension adds a new button to the Firefox toolbar. When the user clicks the button, we display a popup enabling them to choose an animal. Once they choose an animal, we'll replace the current page's content with a picture of the chosen animal.

To implement this, we will:

  • define a browser action, which is a button attached to the Firefox toolbar.
    For the button we'll supply:
    • an icon, called "beasts-32.png"
    • a popup to open when the button is pressed. The popup will include HTML, CSS, and JavaScript.
  • define an icon for the extension, called "beasts-48.png". This will be shown in the Add-ons Manager.
  • write a content script, "beastify.js" that will be injected into web pages.
    This is the code that will actually modify the pages.
  • package some images of the animals, to replace images in the web page.
    We'll make the images "web accessible resources" so the web page can refer to them.

You could visualise the overall structure of the extension like this:

It's a simple extension, but shows many of the basic concepts of the WebExtensions API:

  • adding a button to the toolbar
  • defining a popup panel using HTML, CSS, and JavaScript
  • injecting content scripts into web pages
  • communicating between content scripts and the rest of the extension
  • packaging resources with your extension that can be used by web pages

You can find complete source code for the extension on GitHub.

To write this extension, you'll need Firefox 45 or newer.

Writing the extension

Create a new directory and navigate to it:

mkdir beastify
cd beastify

manifest.json

Now create a new file called "manifest.json", and give it the following contents:

{

  "manifest_version": 2,
  "name": "Beastify",
  "version": "1.0",

  "description": "Adds a browser action icon to the toolbar. Click the button to choose a beast. The active tab's body content is then replaced with a picture of the chosen beast. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#beastify",
  "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/beastify",
  "icons": {
    "48": "icons/beasts-48.png"
  },

  "permissions": [
    "activeTab"
  ],

  "browser_action": {
    "default_icon": "icons/beasts-32.png",
    "default_title": "Beastify",
    "default_popup": "popup/choose_beast.html"
  },

  "web_accessible_resources": [
    "beasts/frog.jpg",
    "beasts/turtle.jpg",
    "beasts/snake.jpg"
  ]

}
  • The first three keys: manifest_version, name, and version, are mandatory and contain basic metadata for the extension.
  • description and homepage_url are optional, but recommended: they provide useful information about the extension.
  • icons is optional, but recommended: it allows you to specify an icon for the extension, that will be shown in the Add-ons Manager.
  • permissions lists permissions the extension needs. We're just asking for the activeTab permission here.
  • browser_action specifies the toolbar button. We're supplying three pieces of information here:
    • default_icon is mandatory, and points to the icon for the button
    • default_title is optional, and will be shown in a tooltip
    • default_popup is used if you want a popup to be shown when the user clicks the button. We do, so we've included this key and made it point to an HTML file included with the extension.
  • web_accessible_resources lists files that we want to make accessible to web pages. Since the extension replaces the content in the page with images we've packaged with the extension, we need to make these images accessible to the page.

Note that all paths given are relative to manifest.json itself.

The icon

The extension should have an icon. This will be shown next to the extension's listing in the Add-ons Manager (you can open this by visiting the URL "about:addons"). Our manifest.json promised that we would have an icon for the toolbar at "icons/beasts-48.png".

Create the "icons" directory and save an icon there named "beasts-48.png".  You could use the one from our example, which is taken from the Aha-Soft’s Free Retina iconset, and used under the terms of its license.

If you choose to supply your own icon, It should be 48x48 pixels. You could also supply a 96x96 pixel icon, for high-resolution displays, and if you do this it will be specified as the 96 property of the icons object in manifest.json:

"icons": {
  "48": "icons/beasts-48.png",
  "96": "icons/beasts-96.png"
}

The toolbar button

The toolbar button also needs an icon, and our manifest.json promised that we would have an icon for the toolbar at "icons/beasts-32.png".

Save an icon named "beasts-32.png" in the "icons" directory. You could use the one from our example, which is taken from the IconBeast Lite icon set and used under the terms of its license.

If you don't supply a popup, then a click event is dispatched to your extension when the user clicks the button. If you do supply a popup, the click event is not dispatched, but instead, the popup is opened. We want a popup, so let's create that next.

The popup

The function of the popup is to enable the user to choose one of three beasts.

Create a new directory called "popup" under the extension root. This is where we'll keep the code for the popup. The popup will consist of three files:

  • choose_beast.html defines the content of the panel
  • choose_beast.css styles the content
  • choose_beast.js handles the user's choice by running a content script in the active tab

choose_beast.html

The HTML file looks like this:

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="choose_beast.css"/>
  </head>

  <body>
    <div class="button beast">Frog</div>
    <div class="button beast">Turtle</div>
    <div class="button beast">Snake</div>
    <div class="button clear">Reset</div>

    <script src="choose_beast.js"></script>
  </body>

</html>

We just have an element for each animal choice. Note that we include the CSS and JS files from this file, just like a web page.

choose_beast.css

The CSS fixes the size of the popup, ensures that the three choices fill the space, and gives them some basic styling:

html, body {
  width: 100px;
}

.button {
  margin: 3% auto;
  padding: 4px;
  text-align: center;
  font-size: 1.5em;
  cursor: pointer;
}

.beast:hover {
  background-color: #CFF2F2;
}

.beast {
 background-color: #E5F2F2;
}

.clear {
 background-color: #FBFBC9;
}

.clear:hover {
 background-color: #EAEAC9;
}

choose_beast.js

In the JavaScript for the popup, we listen for click events. If the click was on one of our three animal choices, we inject a content script into the active tab. Once the content script is loaded, we send it a message with the animal choice:

/*
Given the name of a beast, get the URL to the corresponding image.
*/
function beastNameToURL(beastName) {
  switch (beastName) {
    case "Frog":
      return browser.extension.getURL("beasts/frog.jpg");
    case "Snake":
      return browser.extension.getURL("beasts/snake.jpg");
    case "Turtle":
      return browser.extension.getURL("beasts/turtle.jpg");
  }
}

/*
Listen for clicks in the popup.

If the click is on one of the beasts:
  Inject the "beastify.js" content script in the active tab.

  Then get the active tab and send "beastify.js" a message
  containing the URL to the chosen beast's image.

If it's on a button which contains class "clear":
  Reload the page.
  Close the popup. This is needed, as the content script malfunctions after page reloads.
*/

document.addEventListener("click", (e) => {
  if (e.target.classList.contains("beast")) {
    var chosenBeast = e.target.textContent;
    var chosenBeastURL = beastNameToURL(chosenBeast);

    browser.tabs.executeScript(null, { 
      file: "/content_scripts/beastify.js" 
    });

    var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
    gettingActiveTab.then((tabs) => {
      browser.tabs.sendMessage(tabs[0].id, {beastURL: chosenBeastURL});
    });
  }
  else if (e.target.classList.contains("clear")) {
    browser.tabs.reload();
    window.close();
  }
});

It uses three WebExtensions API functions:

The content script

Create a new directory, under the extension root, called "content_scripts" and create a new file in it called "beastify.js", with the following contents:

/*
beastify():
* removes every node in the document.body,
* then inserts the chosen beast
* then removes itself as a listener
*/
function beastify(request, sender, sendResponse) {
  removeEverything();
  insertBeast(request.beastURL);
  browser.runtime.onMessage.removeListener(beastify);
}

/*
Remove every node under document.body
*/
function removeEverything() {
  while (document.body.firstChild) {
    document.body.firstChild.remove();
  }
}

/*
Given a URL to a beast image, create and style an IMG node pointing to
that image, then insert the node into the document.
*/
function insertBeast(beastURL) {
  var beastImage = document.createElement("img");
  beastImage.setAttribute("src", beastURL);
  beastImage.setAttribute("style", "width: 100vw");
  beastImage.setAttribute("style", "height: 100vh");
  document.body.appendChild(beastImage);
}

/*
Assign beastify() as a listener for messages from the extension.
*/
browser.runtime.onMessage.addListener(beastify);

The content script adds a listener to messages from the extension (specifically, from "choose_beast.js" above). In the listener, it:

  • removes every element in the document.body
  • creates an <img> element pointing to the given URL, and inserts it into the DOM
  • removes the message listener.

The beasts

Finally, we need to include the images of the animals.

Create a new directory called "beasts", and add the three images in that directory, with the appropriate names. You can get the images from the GitHub repository, or from here:

Testing it out

First, double check that you have the right files in the right places:

beastify/

    beasts/
        frog.jpg
        snake.jpg
        turtle.jpg

    content_scripts/
        beastify.js

    icons/
        beasts-32.png
        beasts-48.png

    popup/
        choose_beast.css
        choose_beast.html
        choose_beast.js

    manifest.json

Starting in Firefox 45, you can install extensions temporarily from disk.

Open "about:debugging" in Firefox, click "Load Temporary Add-on", and select your manifest.json file. You should then see the extension's icon appear in the Firefox toolbar:

Open a web page, then click the icon, select a beast, and see the web page change:

Developing from the command line

You can automate the temporary installation step by using the web-ext tool. Try this:

cd beastify
web-ext run

Document Tags and Contributors

Tags: 
 Last updated by: andrewtruongmoz,