Add-ons using the techniques described in this document are considered a legacy technology in Firefox. Don't use these techniques to develop new add-ons. Use WebExtensions instead. If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions.

Starting from Firefox 53, no new legacy add-ons will be accepted on (AMO) for desktop Firefox and Firefox for Android.

Starting from Firefox 57, only extensions developed using WebExtensions APIs will be supported on Desktop Firefox and Firefox for Android.

Even before Firefox 57, changes coming up in the Firefox platform will break many legacy extensions. These changes include multiprocess Firefox (e10s), sandboxing, and multiple content processes. Legacy extensions that are affected by these changes should migrate to use WebExtensions APIs if they can. See the "Compatibility Milestones" document for more information.

A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.

The install.rdf File

In the last section we looked at the contents of the Hello World extension. Now we'll look into its files and code, starting with the install.rdf file. You can open it with any text editor.

The file is formatted in a special flavor of XML called RDF. RDF used to be the central storage mechanism for Firefox, but it is now being replaced for a simpler database system. We'll discuss both of these storage systems further ahead in the tutorial.

Now let's look at the important parts of the file.


This is the unique identifier for the extension. Firefox needs this to distinguish your extension from other extensions, so it is required that you have an ID that is unique.

There are two accepted standards for add-on ids. One is the email-like format in the Hello World example, which should be something like <project-name>@<yourdomain>. The other standard practice is to use a generated UUID string, which is extremely unlikely to be duplicated. Unix-based systems have a command line tool called uuidgen that generates UUIDs. There are also downloadable tools for all platforms that generate them. The enclosing brackets are just notation, and they're just common practice. As long as your id has some uniqueness to it, it's OK to use either form.

<em:name>XUL School Hello World</em:name>
<em:description>Welcome to XUL School!</em:description>

This is the data that is displayed before and after the extension is installed, that you can see in the Add-ons Manager. There are many other tags that can be added, for contributors and translators. The full specification of the install.rdf file has all the details.

Since extensions can be translated to multiple languages, it is often necessary to translate the extension's description, or even its name. A localized description and name can be added with the following code:

    <em:name>XUL School Hola Mundo</em:name>
    <em:description>Bienvenido a XUL School!</em:description>

The es-ES locale string indicates that this is the Spanish (es) localization for Spain (ES). You can add as many <em:localized> sections as you need. For Firefox 2, localizing this file is a little more complicated. We'll discuss localization further ahead in this section.


This specifies that the add-on being installed is an extension. You can read about different possible types in the install.rdf specification.


This node specifies the target application and target versions for the extension, specifically Firefox, from version 4 up to version 10. The UUID is Firefox's unique ID. Other Mozilla and Mozilla-based applications such as Thunderbird and Seamonkey have their own. You can have an extension that works on multiple applications and versions. For example, if you create a Firefox extension, it would normally take little effort to port it to SeaMonkey, which has very similar features and UI.

The min and max version specify the version range in which the extension can be installed. Here's more about the version format. If the application or version range don't match, you won't be allowed to install the extension, or the extension will be installed in a disabled state. Users can disable version checks through preferences or installing add-ons like the Add-on Compatibility Reporter. Beginning with Firefox 11, add-ons will default to compatible and Firefox will mostly ignore the version range. Testing your add-ons with every Firefox version is always recommended, though.

This is the information Firefox and other Mozilla applications need to install an add-on. Any errors or missing information will cause the installation process to fail, or the extension to be installed in a disabled state.

The chrome.manifest File

Chrome is the set of user interface elements of the application window that are outside of a window's content area. Toolbars, menu bars, progress bars, and window title bars are all examples of elements that are typically part of the chrome.

Taken from Chrome Registration.

In other words, the chrome is everything you see in Firefox. All Firefox windows can be seen as having two parts: (1) the chrome and (2) possibly a content area, like the one that displays web pages in a Firefox tab. Windows like the Downloads window are pure chrome. Most of the code for an extension resides in the chrome folder, just like in the Hello World example.

As we saw in the directory structure of the unpacked extension, the chrome is composed of 3 sections: content, locale and skin. The 3 are necessary for most extensions. If we open the chrome.manifest file (again, any text editor will do), we see that the same 3 sections are mentioned:

content   xulschoolhello              content/
skin      xulschoolhello  classic/1.0 skin/
locale    xulschoolhello  en-US       locale/en-US/

The chrome.manifest file tells Firefox where to look for chrome files. The text is spaced to look like a table, but that is not necessary. The parser ignores repeated spaces.

The first word in a line tells Firefox what it is that is being declared (content, skin, locale, or others mentioned later on). The second is the package name, which we will explain shortly.

Skin and locale packages have a third value to specify what locale or what skin they are extending. There can be multiple skin and locale entries relating to different skins and locales. The most common case is having one skin entry for the global skin, classic/1.0, and multiple locale entries, one for each translation. Finally, the location is specified.

There are some additional options that can be included in the entries of a chrome.manifest file. They are documented in the Chrome Registration page. Notably, we can have entries that are OS-specific. This is important because the appearance of the browser is very different for each operating system. If our extension needed to look differently on different systems, we could change the manifest file so that it looks like this:

content   xulschoolhello              content/
skin      xulschoolhello  classic/1.0 skin/unix/
skin      xulschoolhello  classic/1.0 skin/mac/ os=Darwin
skin      xulschoolhello  classic/1.0 skin/win/ os=WinNT
locale    xulschoolhello  en-US       locale/en-US/

This way we can have separate skins for Windows, Mac OS X, and Linux (plus other unix-like systems), each defined in a separate directory. Since most other systems are Unix-based, the "unix" skin is the default, with no flags.

The Chrome

As mentioned earlier, the chrome is composed of 3 sections: content, locale and skin. The content is the most important section, holding user interface (XUL) and script (JS) files. The skin section has the files that define most of the look and feel of the UI (using CSS and images, just like web pages). Finally, the locale section holds all text used in the extension, in DTD and properties files. This division allows other developers to create themes that replace skins, and translators to create localizations in different languages, all of this without having to change your extension or your code. This gives Firefox extensions a great deal of flexibility.

Chrome files are accessed through the chrome protocol. This is what a chrome URI looks like:


So, for instance, if I want to access the file browserOverlay.xul in the extension, the chrome URI would be chrome://xulschoolhello/content/browserOverlay.xul.

If you have too many files in the content and you want to organize them in subdirectories, there's nothing you need to change in chrome.manifest, all you need is to add the right path after content in the URI.

Skin and locale files work in the same way, and you don't need to specify skin names or locale names. So, to access the DTD file in the Hello World extension, the chrome path is chrome://xulschoolhello/locale/browserOverlay.dtd. Firefox knows what locale to look for.

Here's an interesting experiment. Open a new Firefox tab, type chrome://mozapps/content/downloads/downloads.xul on your location bar and press ENTER. Surprised? You just opened the Downloads window in a Firefox tab! You can access any chrome file by just typing its URI in the location bar. This can come in handy if you want to inspect script files that are part of Firefox, other extensions, or your own. Most of these files are opened as text files, with the exception of XUL files, which are executed and displayed like you would normally see them on a window.


There are 2 files in the content directory. Let's look at the XUL file first.

XUL files are XML files that define the user interface elements in Firefox and Firefox extensions. XUL was inspired by HTML, so you'll see many similarities between the two. However, XUL is also an improvement over HTML, having learned from many of the mistakes made during the evolution of HTML. XUL allows you to create richer and more interactive interfaces than the ones you can create with HTML, or at least XUL makes it easier.

XUL files usually define one of two things: windows or overlays. The file you opened before, downloads.xul, has the code that defines the Downloads window. The XUL file included in the Hello World extension is an overlay. An overlay extends an existing window, adding new elements to it or replacing some of the elements in it. The line that we skipped in the chrome.manifest file states that this XUL file is an overlay for the main browser window:

overlay chrome://browser/content/browser.xul  chrome://xulschoolhello/content/browserOverlay.xul

With this line, Firefox knows that it needs to take the contents of browserOverlay.xul and overlay it on the main browser window, browser.xul. You can declare overlays for any window or dialog in Firefox, but overlaying the main browser window is the most common case by far.

Now let's look at the contents of our XUL file. We'll skip the first few lines because they relate to skin and locale, and we'll cover them later.

<overlay id="xulschoolhello-browser-overlay"  xmlns="">

The root element in the file is an overlay. Other XUL documents use the window or dialog tag. The element has a unique id, which you should have on most elements in your XUL. The second attribute is the namespace, which is something you should always define in your XUL root element. It says that this node and all child nodes are XUL. You only need to change namespace declarations when you mix different types of content in the same document, such as XUL with HTML or SVG.

You may have noticed the naming we use on several places, such as the id xulschoolhello-browser-overlay. This is the namespacing standard that we use to avoid conflicts with Firefox and other extensions, as well as making some development tasks easier. We namespace all ids and style classes in overlay elements because they will be mixed with other elements in the main browser window. If we used generic ids like container or input, they will likely conflict with ids used within Firefox, or with ids from other extension overlays. Using namespaces minimizes compatibility problems with other extensions. We use camel casing for file names, and all lower case with dashes for element ids and CSS style class names, but you're free to use your own system.
<script type="application/x-javascript"   src="chrome://xulschoolhello/content/browserOverlay.js" />

Just like in HTML, this includes a JavaScript script file. You can have as many script elements in a XUL document as you need. We'll look into its code later.

You also probably noticed how we format our code, and wonder about the rules we follow. Our general rule on line length is not having lines longer than 80 characters. This feels very restrictive, specially with XML files, but the number was chosen to allow pretty much any text editor to handle these files easily. Even old command line editors work well with files that cut their lines at 80 characters. The tabulation is very straightforward: 2 blank space indents. We never use actual tab characters, with the exception of Makefiles, which will be covered later on. Most of our coding standards are based on Mozilla's or other known and used standards.

We'll skip some code that is covered in the locale section, moving on to the most important part of the content:

<menubar id="main-menubar">
  <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;"
    accesskey="&xulschoolhello.helloMenu.accesskey;" insertafter="helpMenu">
      <menuitem id="xulschoolhello-hello-menu-item"
        oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" />

<vbox id="appmenuSecondaryPane">
  <menu id="xulschoolhello-hello-menu-2" label="&xulschoolhello.hello.label;"
      <menuitem id="xulschoolhello-hello-menu-item-2"
        oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" />

This is the code that adds the Hello World menu to the browser window.

There are two similar code blocks, because in modern versions of Firefox, particularly on Windows, a single Firefox menu button is presented, with simplified menu options, rather than an extensive menu bar. The second code block covers the common menu button case; the first code block covers all other cases. Check Menu Bar under the Options menu of the menu button to toggle display of the classic menu on Windows and some Linux distributions.

In order to write this code, we needed some knowledge of the XUL code in browser.xul. We needed to know that the id of the right pane in the unified menu is appmenuSecondaryPane. We're adding a menu of our own, and telling Firefox to add it in that pane, right after the Add-ons item. That's the purpose of the attribute:


appmenu_addons is the id of the menu element that corresponds to the Add-ons menu item in the main menu. We'll see later how we can find out things like the ids of browser elements, but for now let's look at the elements that compose the Hello World menu.

For the classic menu, we added the Hello World menu right in the "root" of the menu so that it would be very easy for you to spot it, but this is not a recommended practice. Imagine if all extensions added menus to the top menu; having a few extensions would make it look like an airplane dashboard, full of knobs and switches. In the case of the unified menu, things are a little more difficult due to lack of options. If your menu item fits in the Web Developer section, it is recommended that you add it there. Otherwise, the root menu might be your only recourse.

One recommended location for menus in the classic menu vase is under the Tools menu, so the code should really look like this:

<menupopup id="menu_ToolsPopup">
  <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;"
      <menuitem id="xulschoolhello-hello-menu-item"
        oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" />

We're overlaying the menu that is deeper into the XUL tree, but it doesn't matter because all we need is the id of the element we want to overlay. In this case it is the menupopup element that's inside of the Tools menu element. The insertbefore attribute tells Firefox to add the menu at the bottom of the dev tools section, above its end separator. We'll discuss more about menus later on in the tutorial.

Now let's look at the actual code:


This attribute defines an event handler. The command event is the most frequently used in Firefox, since it corresponds to the main action for most UI elements. The value of the attribute is JavaScript code that invokes a function. This function is defined in the JS file that was included with the script tag. The JS function will be called once the user clicks on the menu item in the Hello World menu. All event handlers define a special object named event, which is usually good to pass as an argument to the function. Event handlers are explained in greater depth further ahead.

Now let's look at the JavaScript file and see what's going on when the event is fired.

 * XULSchoolChrome namespace.
if ("undefined" == typeof(XULSchoolChrome)) {
  var XULSchoolChrome = {};

The XULSchoolChrome namespace is defined. All objects and variables we define in this JavaScript are global, meaning that scripts in Firefox and other extensions can see them and interact with them. This also means that if we define an object called MenuHandler or some other generic name, it's likely going to conflict with an existing object. What we do here is define a single global object: XULSchoolChrome. Now we know that all of our objects are inside this object, which is unlikely to be duplicated or overwritten by other extensions.

You can read more about the typeof operator. If you're unfamiliar with JavaScript or this particular syntax, initializing an object as {} is the equivalent of initializing it to new Object().

 * Controls the browser overlay for the Hello World extension.
XULSchoolChrome.BrowserOverlay = {

Finally, BrowserOverlay is our object. Naming and referencing objects in such a long and verbose manner can feel uncomfortable at first, but it's worth the cost.

We use Javadoc style comments on all namespaces, objects and object members. This is a similar standard to the one used in Mozilla code, and some tools can generate documentation automatically from Javadoc.
sayHello : function(aEvent) {
  let stringBundle = document.getElementById("xulschoolhello-string-bundle");
  let message = stringBundle.getString("xulschoolhello.greeting.label");


And, finally, this is our function declaration. Three lines of code are all we need for it to work. The first line in the body of the function declares a variable that will hold the stringbundle element defined in the overlay. The variable is declared using let, which is similar to var but with more restricted scope. Here you can read more about let declarations.

Just like in regular JS, we can use the DOM (Document Object Model) in order to manipulate the XUL document. First we get a reference of the stringbundle element in the document. This is a special element that allows us to obtain localized strings dynamically, by only providing a "key" that identifies the string. This is what we do on the second line. We call the getString method of the bundle element and get the localized message to be displayed. We then call the window.alert function with the message, just like we would do in an HTML document.


There are two types of locale files: DTD and properties, and in this example we use them both. DTD is the most efficient way of showing text in XUL, so you should use it whenever possible. It is somewhat inflexible so it can't be used for dynamically generated text, hence the need for an alternate way of getting localized strings.

Looking back at the menu code, you probably noticed some attributes such as this:

label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;"

These attributes define the text that you see on the menus, and they are string keys that are defined in our DTD file, browserOverlay.dtd. The DTD file was included in the XUL file with the following code:

<!DOCTYPE overlay SYSTEM "chrome://xulschoolhello/locale/browserOverlay.dtd" >

And in the DTD file you can see the association between keys and localized strings:

<!ENTITY xulschoolhello.hello.label            "Hello World!">
<!ENTITY xulschoolhello.helloMenu.accesskey    "l">
<!ENTITY xulschoolhello.helloItem.accesskey    "H">

Notice that on the XUL file you enclose the string key with & and ; while on the DTD file you only specify the key. You may get weird parsing errors or incorrect localization if you don't get this right.

Access keys are the shortcuts that allow you to quickly navigate a menu using only the keyboard. They are also the only way to navigate a menu for people with accessibility problems, such as partial or total blindness, or physical disabilities that make using a mouse very difficult or impossible. You can easily recognize the access keys on Windows because the letter that corresponds to the access key is underlined, as in the following image:

Most user interface controls have the accesskey attribute, and you should use it. The value of the access key is localized because it should match a letter in the label text. You should also be careful to avoid access key repetition. For example, within a menu or submenu, access keys should not be repeated. In a window you have to be more careful picking access keys because there are usually more controls there. You have to be specially careful when picking access keys on an overlay. In our case, we can't use the letter "H" as an accesskey in the Main menu item, because it would be the same as the access key in the Help menu. Same goes with "W" and the Window menu on Mac OS. So we settled on the letter "l".

DTD strings are resolved and set when the document is being loaded. If you request the label attribute value for the Hello World menu using DOM, you get the localized string, not the string key. You cannot dynamically change an attribute value with a new DTD key, you have to set the new value directly:

let helloItem = document.getElementById("xulschoolhello-hello-menu-item");

// The alert will say "Hello World!"
// Wrong
helloItem.setAttribute("label", "&xulschoolhello.hello2.label;");
// Better
helloItem.setAttribute("label", "Alternate message");
// Right!
helloItem.setAttribute("label", someStringBundle.getString("xulschoolhello.hello2.label"));

This is the reason DTD strings are not a solution for all localization cases, and the reason we often need to include string bundles in XUL files:

<stringbundleset id="stringbundleset">
  <stringbundle id="xulschoolhello-string-bundle"
    src="chrome://xulschoolhello/locale/" />

The stringbundleset element is just a container for stringbundle elements. There should only be one per document, which is the reason why we overlay the stringbundleset that is in browser.xul, hence the very generic id. We don't include the insertbefore or insertafter attributes because the ordering of string bundles doesn't make a difference. The element is completely invisible. If you don't include any of those ordering attributes in an overlay element, Firefox will just append your element as the last child of the parent element.

All you need for the string bundle is an id (to be able to fetch the element later) and the chrome path to the properties file. And, of course, you need the properties file:

xulschoolhello.greeting.label = Hi! How are you?

The whitespace around the equals sign is ignored. Just like in install.rdf, comments can be added using the # character at the beginning of the line. Empty lines are ignored as well.

You will often want to include dynamic content as part of localized strings, like when you want to inform the user about some stat related to the extension. For example: "Found 5 words matching the search query". Your first idea would probably be to simply concatenate strings, and have one "Found" property and another "words matching..." property. This is not a good idea. It greatly complicates the work of localizers, and grammar rules on different languages may change the ordering of the sentence entirely. For this reason it's better to use parameters in the properties: = Found %S words matching the search query!

Then you use getFormattedString instead of getString in order to get the localized string. Thanks to this we don't need to have multiple properties, and life is easier for translators. You can read more about it on the Text Formatting section of the XUL Tutorial. Also have a look at the Plurals and Localization article, that covers a localization feature in Firefox that allows you to further refine this last example to handle different types of plural forms that are also language-dependent.


Styling XUL is very similar to styling HTML. We'll look into some of the differences when we cover the XUL Box Model, and other more advanced topics. There isn't much styling you can do to a minimal menu and a very simple alert message, so the Hello World extension only includes an empty CSS file and the compulsory global skin file:

<?xml-stylesheet type="text/css" href="chrome://global/skin/"  ?>
<?xml-stylesheet type="text/css" 
  href="chrome://xulschoolhello/skin/browserOverlay.css"  ?>

The global skin CSS file holds the default styles for all XUL elements and windows. Forgetting to include this file in a XUL window usually leads to interesting and often unwanted results. In our case we don't really need to include it, since we're overlaying the main browser XUL file, and that file already includes this global CSS. At any rate it's better to always include it. This way it's harder to make the mistake of not including it. You can enter the chrome path in the location bar and inspect the file if you're curious.

This covers all of the files in the Hello World extension. Now you should have an idea of the basics involved in extension development, so now we'll jump right in and set up a development environment. But first, a little exercise.


Make the following changes to the example extension:

  • Edit the welcome message that is displayed in the alert window.
  • Move the Hello World menu to the Tools menu, where it belongs.

Repackage the XPI. Issue the following command from within the extension root directory on Linux or Mac OS X:

zip -r ../xulschoolhello2.xpi * 

On Windows, use a ZIP tool to compress all files and subdirectories within the extension root directory. Name the file with extension .xpi

Re-install the XPI. You can just drag the XPI file to the browser and it will be installed locally.

Test it and verify your changes worked. If you run into problems at installation, it's likely that you didn't reproduce the XPI structure correctly, maybe adding unnecessary folders.

Note: Packaging an extension is really just creating a ZIP archive of the contents of the main folder, then changing the filename extension to .XPI. Do not zip the containing folder, just its contents. The content folder, chrome.manifest, install.rdf, and other files and directories should be at the root level of your archive. If you zip the containing folder, your extension will not load.

Note that the Tools menu is hidden by default on Firefox 4 and above, on Windows and some Linux distributions. Check Menu Bar under the Options menu of the Firefox menu button to enable it.

Once you're done, you can look at this reference solution: Hello World 2.

This tutorial was kindly donated to Mozilla by Appcoast.

Document Tags and Contributors

 Last updated by: wbamberg,