Browser extensions are an important part of the web ecosystem. They make our lives easier and enhance our web browsing experience with functionalities that not natively available. Extensions like grammar checkers, ad blockers, note-taking tools, and screenshot capture help make browsing more efficient and informative.
Developing browser extensions can be quite rewarding as well. However, compared to modern web development with frameworks and rich toolsets, extension development can feel less streamlined.
For example, the manifest.json
file, while essential, can be cumbersome to manage for complex extensions. Defining permissions, background scripts, and content scripts can get intricate. Moreover, interacting directly with the underlying APIs can get quite complicated.
In this tutorial, we’ll explore a library called WXT that helps make the process of developing browser extensions smoother and more efficient. We’ll build an explanatory project to help us better understand how to use WXT. You can find the complete demo code here.
WXT aims to streamline the development process for browser extensions. It is an open source library that can scaffold a project easily and set you up with a project structure all while improving the development experience.
The WXT library uses Vite under the hood to provide features like HMR, which can be crucial when dealing with complex web extensions. On top of that, WXT allows you to use any framework of your choice, including React, Svelte, Vue, and more.
Here’s how WXT helps simplify browser extension development:
Moreover, WXT allows you to build extensions for any browser and manifest version combination.
Let’s take a look at how WXT does all of these and then, with the help of an example try to understand how it works.
WXT allows you to scaffold a project based off various templates like React, Vue, Svelte. We’ll create an explanatory project that uses vanilla TypeScript to do some fundamental web extension work, like manipulating content and passing messages:
npx WXT@latest init --template vanilla
After this, you simply need to install the dependencies using the npm i
command.
We’ll start up the dev server using the npm run dev
command. By default, WXT builds the extension for Chrome. The npm run dev
command should open Chrome, where you can use the extension that was just scaffolded. Now, let’s try to launch the same extension in Firefox:
npm run dev:firefox
You should see the same extension launched in Firefox, like so:
If you notice in the .output
folder that was just created, WXT creates the manifest for us. You can see that for Chrome, it uses a Manifest V3, and for Firefox, it uses Manifest V2.
Now let’s understand what’s going on here.
When you scaffold a WXT project, you get a project structure, based on which WXT generates the manifest file.
In the context of the WXT framework, an entry point refers to any HTML, JavaScript, or CSS file that needs to be included and bundled with the final extension. By default, WXT expects entry points to be placed in a folder named entrypoints/
within your project structure.
WXT can automatically update your extension’s manifest.json
file based on specific naming conventions for entry point files. For instance:
popup.html
gets mapped to the action.default_popup
property in the manifestcontent.ts
gets mapped to content_scripts.[name].js
where [name]
is a unique identifierThe manifest.json
file is used to define how different entry points contribute to the extension’s functionality. For instance, a popup.html
file gets mapped to the popup functionality, and content scripts get registered based on their filenames.
In the last project that was created for us during the setup section above, the entrypoints
folder has a background.ts
file, which is a background script.
Background scripts in WXT play an essential role in enabling your extension to perform tasks in the background, independent of the browser tab or window that’s currently active.
Background scripts are ideal for tasks that take a long time to complete, such as:
The way WXT implements background scripts depends on the manifest version or browser you’ve chosen for your extension’s development:
In MV2, background scripts are implemented as a single JavaScript file referenced in the background
property of your extension’s manifest.json
file. This file acts as a dedicated background page that executes throughout the extension’s lifetime.
MV3 introduces service workers as the primary mechanism for background functionality. Service workers are event-driven scripts that can run in the background even when the extension’s UI is not active.
The .output
folder’s manifest.json has the following lines for the Chrome and Firefox extension, respectively.
For chrome-mv3
:
"background": { "service_worker": "background.js" },
For firefox-mv3
:
"background": { "scripts": [ "background.js" ] },
You can check the logs in the dev console for the service worker if you’re on Chrome, which you get in the Manage extensions menu.
Content scripts are a crucial component of extensions that allow you to modify the content and behavior of web pages that users visit. They allow for DOM manipulation and page enhancements.
defineContentScript
has the main method, which is executed when the URL matches the path mentioned in the content.ts
file:
matches: ['*://*.google.com/*'],
In this case, the console log can be seen if you navigate to google.com. When WXT complies the extension, this is added to the manifest in firefox-mv2
:
"content_scripts": [ { "matches": [ "*://*.google.com/*" ], "js": [ "content-scripts/content.js" ] } ],
WXT interacts with the Chrome Extensions APIs to enable various functionalities within your extension. You can access these functionalities through WXT’s own functions and methods, such as the defineBackground
and defineContentScript
functions we saw above.
WXT provides the global browser
to interact with the underlying browser API, which means that the code is consistent for all browsers. For example, WXT facilitates communication between content scripts, background scripts, and the popup UI using the runtime messaging API.
Additionally, WXT provides a simplified API for managing storage in your web extensions, built on top of the browser’s storage API. This WXT storage API offers a more developer-friendly way to store and retrieve data for your extension. It can be manually imported using the command below:
import { storage } from 'WXT/storage';
Here’s an example of how to use this:
await storage.getItem('local:test');
The command above would get the item from current browser profile and doesn’t sync across devices. It’s important to add the local
prefix to the key here. Similarly, session:
, sync:
, or managed:
could be used.
The terms “session,” “sync,” and “managed storage” refer to different types of storage areas available for use by extensions to store and manage data. Each type of storage has specific characteristics and use cases:
session
storage is temporary and only lasts for the duration of the browser session. The data is lost when the browser is closed. It’s ideal for storing data that does not need to persist across browser restarts, such as temporary state or transient informationsync
storage is synchronized across all instances of the browser where the user is logged in with the same account. The data is persisted even after the browser is closed and reopened. It’s suitable for storing settings or state that should be consistent across multiple devices, such as user preferences or bookmarksmanaged
storage is a special storage area that is controlled by the system administrator or organization policies. The data is managed by external policies and can persist or change based on administrator configuration. It’s mostly used in enterprise environments where extensions need to adhere to organization-wide policies, providing a way to enforce settings or configurations centrallyLet’s take a closer look at how we can use browser
to make different parts of the extension communicate with one another. We’ll make some changes to various files in our project.
First, let’s update the content.ts
file:
export default defineContentScript({ matches: ['*://*.google.com/*'], main() { console.log('content script'); const button = document.createElement('button'); button.textContent = 'Click me to send message'; button.addEventListener('click', handleClick); document.body.appendChild(button); function handleClick() { console.log("button clicked"); browser.runtime.sendMessage({ message: Date.now() }); } }, });
In the code above, we added a new button to the webpage. We also added a click event listener to the button, which sends a message when clicked using the browser.runtime
API. The function takes an object containing the message data as an argument. In this case, the message is the current timestamp.
Next, we’ll update the background.ts
file:
export default defineBackground(() => { browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.message) { console.log('Content script sent message:', message); } }); });
Above, we used the browser.runtime.onMessage.addListener
function to listen for incoming messages. This function takes a callback that executes whenever a message is received.
When we run the development server and navigate to google.com, we get our button at the bottom, which we added via the content script. Clicking on it gives us the logs in the dev console as well the console for the background service worker.
While WXT doesn’t directly support sending messages from a content script to the popup UI in a single step, you can achieve this communication by leveraging the background script as an intermediary:
Let’s look at another example that passes messages from the background to the content script. To send messages to the content script, we need to specify which tab the request applies to. tabs.sendMessage()
is used instead of runtime.send()
.
Update the content.ts
file as follows:
export default defineContentScript({ matches: ['*://*.google.com/*'], main() { console.log('content script'); const timeStampDiv = document.createElement('div'); timeStampDiv.textContent = ''; document.body.appendChild(timeStampDiv); browser.runtime.onMessage.addListener( function(request, sender, sendResponse) { console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension"); timeStampDiv.textContent = "time stamp is " + request.message; } ); }, });
Then, update the background.ts
file like so:
export default defineBackground(() => { console.log('Hello background!', { id: browser.runtime.id }); setInterval(async () => { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); if (tab.id) { browser.tabs.sendMessage(tab.id, { message: Date.now() }); } }, 1500); });
Here, we simply updated our code to send a new timestamp every 1.5
seconds to the content script, which updates a div
element on the webpage.
Using a frontend framework with the WXT library can significantly improve the development process and the quality of your web extension. This approach allows for better code organization, easier state management, and a more responsive and attractive user interface, ultimately leading to a more maintainable and scalable project:
WXT is designed to work well with various custom frontend libraries, providing flexibility in building web extension UIs. For example, scaffolding a project with React is as simple as the following:
npx WXT@latest init --template react
WXT offers a compelling solution for streamlining browser extension development. It provides a layer of abstraction, handles common tasks, and automates build and packaging processes, empowering developers to focus on core functionalities and deliver exceptional extensions.
The flexibility offered by the WXT library allows you to leverage various frontend libraries for building rich and interactive UIs. Whether you’re building your very first browser extension or your next of many, WXT can significantly enhance your DX and efficiency as you develop powerful browser extensions.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.