The Internet has all the tutorials to guide you through building your first Chrome extension. In my case, I simply stuck to the documentation, browsing other people's experiences for solutions when something went unexpected. There are things I wish I had known at the beginning, but it's never too late to share.

1. Setting up code completion in IDE

If your editor supports IntelliSense, hooray! The first step would be setting up type acquisition in the config file. For VSCode users, just add the following code to a jsconfig.json file in the workspace:

Installing @types/chrome as a dev-dependency will do the same.

Now you're equipped with Chrome API code completion.

type acquisition.gif

2. Page action may not colorize your icon as expected

When using pageAction, the icon will automatically colorize when users visit the URLs matched in permission, indicating that the extension is now active. The trick is, permission states all the URLs you ask to access – including those that you perform requests to. The truth is, no one wants their users bumping into a URL and happen to find out that the extension icon is clickable, but is either not working or out of the use case.

In my case, I want the icon active only when the content script is running, and it only runs on one single site. If you use popup.html as a setting panel like I do, then there is no point to bother users with settings when they don't need them. It would be ideal if the page_action field in manifest.json allows developers to define which page to activate the extension, instead of activating the whole host permission set.

To deal with the situation I had to switch to browser_action, manually colorizing and graying out the icon by calling browserAction.setIcon. Otherwise, the icon would be lighted up the whole time.

Now that we switched to browser_action, notice that you also need to pass a tabid to tell the browser which tab you would like your icon to shine. To do that, we need to establish communication between content script and background.

Tell the background script to deal with the icon upon connection.

3. You can decide run time for each content script

By default, content scripts are scheduled to be injected when DOM is loaded but before window.onload event fires. That means you don't need to put everything inside a DOMContentLoaded event handler. The other timing options are document_start, which is before any other scripts get injected, and document_end, which is before any image assets have loaded.

4. Most tabs APIs do not require permission declaration

Knowing exactly what permission your extension needs is substantial since you would have to clarify why you need some of the permission upon package submission. The permission justification in Chrome Web Store says:

Remove any permission that is not needed to fulfill the single purpose of your extension. Requesting an unnecessary permission will result in this version being rejected.

Some common actions require the id of a tab, and the communication between a popup and a content script is among them. Luckily, for this kind of actions you can just call tabs.query without any permission declaration in the manifest file. However, if you request to read properties regarding tab contents such as URL and title, you would still need to ask for permission.

5. Inspect popup in a more comfortable way

The official way to inspect a popup is to right-click it and choose 'inspect popup window'. But inspecting the elements in a new window is not as convenient as it is in the native window devtool. Moreover, the devtool will disappear once the popup invalidates. To ease the pain, you could instead just visit the URL of the popup in the address bar:

extension://{extension ID}/{path to the popup HTML file}

...or simply type location.href in the popup devtool console and copy-paste the URL to the main browser window.

6. Avoid innerHTML, eval and inline Javascript

Yes, you already know about the risk of XSS (Cross Site Scripting) attack caused by the careless innerHTML and eval. The same rule applies to extension development. Vanilla DOM insertion could be a hassle especially when you need to create lots of elements in the extension. A component system may waive you from the physical labor if you write inside frameworks, but if you insist in vanilla, here are some suggestions by Mozilla that come to rescue.

Note that when submitting your extension to the Firefox add-on platform, it would check if your code contains innerHTML – any existence of it will trigger a warning even when there is no unsanitized third-party data.

On the other hand, Chrome will not execute any inline Javascript such as <button onclick="..."></button>. To clarify, Chrome 46+ does relax the policy: that is, if you encode your source code in base64 hash and declare script policy in manifest.json... but why would you?

7. Pitfalls in submitting the package to Firefox

Submitting the final package to Chrome Web Store is smooth, but doing it in Firefox's add-on platform is a whole other story.

Firefox states that it allows .crx .zip and .xpi package. But I encountered an 'unknown error' when submitting the same zip I uploaded to Chrome. Later it turned out I had to specifically change the extension name of the zip file to .xpi, which is basically a .zip with a different name. (Why say you love me when you don't, Mozilla?)

It then threw an error about an invalid manifest.json. After double-checking the Chrome incompatibility sheet without luck, I realized Firefox does not take a compressed directory – you have to compress the files, not the directory they are in. I admit it's dumb. I meant me.

8. Give me some extension examples!

Chrome has put up many sample extensions that exercise different APIs. Learning by example is always a confidence booster.

Happy coding, first-timers! In case you are interested, here is the first extension I published a while ago: Popcorn (Chrome and Firefox).

Some rights reserved
Except where otherwise noted, content on this page is licensed under a Creative Commons Attribution-NonCommercial 4.0 International license.