Chrome Extension 101: Important Concepts Before You Create Your Extension

Mario Gunawan
4 min readFeb 12, 2024
AI-Generated using pixir

At the beginning of this year, I decided to create my first Chrome extension. So I’ve built one in 1 month and decided to share with you guys my findings on what I wish I had known before I created the extension so that you can move faster. One thing to note though, is that I highly recommend you finish the getting started section of the Chrome extension first before you read this article.

Options Page

A full page for Chrome extension

A Chrome extension can have its dashboard page, and you can configure it using the options_page property on the manifest.

Minimum Chrome Version

Always make sure that the minimum_chrome_version in your manifest.json supports all the features you’re building on. You can check the minimum Chrome version from the little badge below the function you want to use. For example, chrome.tts.speak requires Chrome version 101+.

Playing Audio

In manifest V3, playing audio is a bit trickier than in V2 where all you need to do is create a bg page. In V3, you need to:

  • Create an audio.html file
# Audio.html
<!DOCTYPE html>
<title>Play MP3 File</title>
<script type="module" src="/service_workers/audio/audio.js"></script>
  • Create the audio.js script that listens to messages
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.playAudio && === "offscreen") {
sendResponse("Audio played");

function playAudio({ source, volume }) {
const audio = new Audio(source);
audio.volume = volume;;
  • Then, on your service worker, whenever you want to play an audio, you got to create an offscreen document first, then play the audio
async playAudio() {
if (!(await chrome.offscreen.hasDocument())) { // prevent double creation
await chrome.offscreen.createDocument({
url: "html/audio.html",
reasons: [chrome.offscreen.Reason.AUDIO_PLAYBACK],
justification: "play sound effects",

const audioMessage: AudioPlayRequest = {
target: "offscreen",
playAudio: { source: "/abc.mp3", volume: 1 }, // volume is 0 to 1

await chrome.runtime.sendMessage(audioMessage);

Credit to

Use for Configuration

Do not use localStorage because it’s not available on service workers, instead use either or . The difference is local is only for this machine, and sync is synced to any browser the user has logged into. Read more on this here.

Having multiple service worker

If you want to separate your service workers into multiple files, it’s doable using the import statement in your service worker.

// background.js
import ".sw_abc";
import ".sw_defg";
// sw_abc.js and sw_defg.js

function main() {
) => { /* do something... */ });

main(); // do not forget to call main

Careful of return true in onMessage

In the documentation, it is said that you can return true if the code is asynchronous and then send response once the result is done.

// inside the onMessage listener
switch (message.action) {
case "GET_VOLUME": {
.then((volume) => {
success: true,
.catch((err) => sendResponse({ success: false, error: err }));

return true; // we return this to mark that we're
// going to send a response in the future
// ....

Do NOT under any circumstances return true if you’re not going to send response… here’s what I meant:

switch (message.action) {
// a lot of async cases
return true;

If you do this, then once a message that does not match any action comes, you will still return true , and the sender will expect a response soon. This also gets in the way if you implement multiple service worker like in previous section, since if your request should be handled by sw_defg but sw_abc returns true as a general statement, then chrome will think sw_abc will handle it thus making the request never reach sw_defg.

Doing action on the browser start

On your background.js, add this code:

chrome.runtime.onStartup.addListener(async () => {
// do something.... for example chrome.runtime.openOptionsPage()

You can also separate it into a different file, just like service workers

Creating custom pages

Fun fact: you can have multiple “options” pages and have it opened. Let’s assume we have a html/hello.html in our extension folder. We can open it using this code (in service worker):

url: `chrome-extension://${}/html/hello.html`,

Actually, you can do this to your options page as well, if, for example, your options page is index.html

url: `chrome-extension://${}/index.html`,

If you want to open it via your extension page, just simply use`chrome-extension://${}/index.html`)

Yes, it’s possible to use Framework to create option pages/popup

I created my extension using Vite + React framework, with a couple of tweaks. I’ll create a GitHub repo with my setup (vite, react, bun, tailwind, and typescript) and update this article once I’m done.

Sadly, it’s not possible to use Framework for script injection

I’ve tried injecting a react code to a website but failed because of… some errors (I forgot what the error was… but I spent 1–2 days trying to do this but failed). So I just use plain html injection (element.appendChild(…)).

If you’ve found a way to inject the code please do let us know in the comment.

That’s all, happy coding!



Mario Gunawan

I'm a passionate mobile / web developer. Writing articles about software development, mainly about flutter and react.