Back
Lesson 3

Web Extensions with WXT Library

View Source Code

Overview

Transform your extension development workflow with WXT - a modern framework that provides hot reloading, cross-browser compatibility, TypeScript support, and optimized builds. This lesson replaces the manual reload cycle with professional tooling while maintaining the same extension functionality.

By the end, you’ll have a development environment that reloads automatically and supports both Chrome and Firefox from a single codebase.

What You’ll Learn

  • Install and configure WXT for extension development
  • Understand WXT’s file-based routing and conventions
  • Use hot module reloading for instant feedback
  • Write browser-agnostic code that works in Chrome and Firefox
  • Navigate WXT’s project structure and build output
  • Rebuild the Lesson 2 extension using WXT best practices

Why WXT?

Vanilla extension development has significant pain points:

Manual Reloading: Every code change requires clicking reload in chrome://extensions

Browser-Specific APIs: chrome.tabs (Chrome) vs browser.tabs (Firefox) requires conditional code

No Build Tools: Missing TypeScript, hot reloading, and modern bundling

WXT solves these problems while letting you write standard extension code - it’s not a wrapper or abstraction, just better tooling.


WXT logo

What is WXT?

WXT is a next-generation web extension framework built on Vite. It provides:

  • Hot Module Reloading - Changes appear instantly without manual reloads
  • Cross-Browser Support - One codebase for Chrome, Firefox, Edge, Safari
  • TypeScript Built-in - Full type safety with zero configuration
  • File-Based Routing - Convention over configuration for extension structure
  • Production Builds - Optimized bundles for store submission

Official Docs: wxt.dev

The framework is opinionated but follows extension best practices. If you follow WXT’s conventions, development becomes significantly faster.


Installing WXT

Create a New Project

Use your preferred package manager to initialize a WXT project:

# Using bun (fastest)
bun create wxt@latest

# Using npm
npm create wxt@latest

# Using pnpm
pnpm create wxt@latest

# Using yarn
yarn create wxt

Project Setup Prompts:

  1. Project name: tabnotes
  2. Template: vanilla (we’ll add frameworks later if needed)
  3. Package manager: Choose your preference (examples use bun)

Install Dependencies

cd tabnotes
bun install  # or npm install, pnpm install, yarn

Starting the Development Server

Run the dev server to launch a Chrome instance with your extension pre-loaded:

bun dev  # or npm run dev, pnpm dev, yarn dev

What Happens:

  1. WXT starts a development server
  2. A new Chrome window opens automatically
  3. Extension is pre-installed and active
  4. Changes to code trigger automatic reloads

The extension opens in an isolated Chrome profile - your personal Chrome browsing remains separate.

Exploring the Starter Extension

Click the extension icon (you may need to pin it from the puzzle icon menu). The starter includes:

  • Animated popup with a counter
  • Background script logging events
  • Content script injecting on google.com

This demonstrates WXT’s capabilities - we’ll simplify to match our Lesson 2 extension.


Understanding WXT’s Project Structure

tabnotes/
├── .output/              # Generated extension files (auto-created)
├── assets/               # Static assets bundled with code
├── components/           # Reusable UI components
├── entrypoints/          # Extension entry points (KEY FOLDER)
│   ├── background.ts     # Background script / service worker
│   ├── content.ts        # Content scripts
│   └── popup/            # Popup UI
│       ├── index.html
│       ├── main.ts
│       └── style.css
├── public/               # Static files copied as-is
│   └── icon/             # Extension icons
├── wxt.config.ts         # WXT configuration
└── package.json          # Project dependencies and scripts

Key Concepts

entrypoints/ Directory: WXT uses file-based routing. Files in entrypoints/ automatically become extension components:

  • background.ts → Background script
  • popup/ → Popup interface
  • content.ts → Content script
  • options.ts → Options page (if created)

No need to register these in manifest.json - WXT handles it.

.output/ Directory: WXT generates the final extension here:

  • .output/chrome-mv3-dev/ - Development build for Chrome
  • .output/firefox-mv3-dev/ - Development build for Firefox
  • Contains auto-generated manifest.json and bundled files

How Hot Reloading Works

Make a change to entrypoints/popup/index.html - maybe change the heading text. Save the file.

Instant Update: The popup refreshes automatically without clicking reload.

Behind the Scenes

  1. WXT watches source files for changes
  2. On change, Vite rebuilds affected modules
  3. WXT updates .output/chrome-mv3-dev/
  4. Extension reloads automatically in Chrome

Limitations:

Hot reloading works for most changes:

  • ✅ HTML, CSS, JavaScript edits
  • ✅ Content script modifications
  • ❌ Manifest/config changes (requires restart)
  • ❌ Background script updates (requires restart)

For manifest or background changes: Ctrl+C to stop dev server, then bun dev to restart.


The Auto-Generated Manifest

Examine .output/chrome-mv3-dev/manifest.json:

{
  "manifest_version": 3,
  "name": "tabnotes",
  "version": "1.0.0",
  "description": "",
  "permissions": ["storage"],
  "action": {
    "default_popup": "popup/index.html"
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [{
    "matches": ["https://google.com/*"],
    "js": ["content-scripts/content.js"]
  }]
}

You didn’t write this - WXT generated it from your project structure and config.

Why Auto-Generation Matters

Different browsers require different manifest formats:

  • Chrome uses service_worker for background
  • Firefox uses scripts array for background

WXT generates the correct format for each browser target automatically.


Writing Cross-Browser Code

The browser vs chrome API

In vanilla extensions, you write:

// Chrome-specific
chrome.tabs.onActivated.addListener(...)

// Firefox-specific  
browser.tabs.onActivated.addListener(...)

WXT’s Solution: Always use browser - WXT polyfills it for Chrome:

// Works in both Chrome and Firefox
browser.tabs.onActivated.addListener(async (activeInfo) => {
  const tab = await browser.tabs.get(activeInfo.tabId);
  console.log('Tab activated:', tab);
});

WXT automatically converts browser to chrome when building for Chrome.


Rebuilding the Lesson 2 Extension

Let’s simplify the starter to match our basic tab-detection extension.

Step 1: Clean Up the Popup

Replace entrypoints/popup/index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>TabNotes</title>
</head>
<body>
  <h1>TabNotes</h1>
</body>
</html>

Delete these unnecessary files:

  • entrypoints/popup/main.ts (we’ll add logic later)
  • entrypoints/popup/style.css (we’ll style later)
  • entrypoints/content.ts (not needed yet)
  • components/ folder (not needed yet)
  • assets/ folder (not needed yet)

Step 2: Update Background Script

Replace entrypoints/background.ts:

// entrypoints/background.ts
export default defineBackground(() => {
  browser.tabs.onActivated.addListener(async (activeInfo) => {
    const tab = await browser.tabs.get(activeInfo.tabId);
    console.log('Tab activated:', tab);
  });
});

WXT-Specific Pattern: The defineBackground() wrapper is WXT’s convention for background scripts. It provides proper TypeScript types and handles service worker registration.

Step 3: Configure Permissions

Update wxt.config.ts:

// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  manifest: {
    permissions: ['tabs']
  }
});

WXT merges this config with auto-generated manifest values.

Step 4: Test the Extension

Your dev server should auto-reload. If not, restart with Ctrl+C then bun dev.

Verify it works:

  1. Extension popup shows “TabNotes” heading
  2. Click “Inspect service worker” in chrome://extensions
  3. Switch between browser tabs
  4. Console logs each tab activation

Congratulations! You’ve rebuilt the Lesson 2 extension with modern tooling.


WXT Project Configuration

The wxt.config.ts file controls WXT’s behavior:

// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  manifest: {
    name: 'TabNotes',
    version: '1.0',
    description: 'Add notes to any tab in your browser',
    permissions: ['tabs'],
  },
});

We’ll expand this config as we add features. For now, the minimal config is sufficient.


WXT vs Vanilla Development

Vanilla Extension (Lesson 2)

  • ❌ Manual reload after every change
  • ❌ Chrome-specific code (chrome.tabs)
  • ❌ Manual manifest management
  • ❌ No TypeScript support
  • ❌ No build optimization

WXT Extension (Lesson 3)

  • ✅ Automatic hot reloading
  • ✅ Cross-browser compatible (browser.tabs)
  • ✅ Auto-generated manifest
  • ✅ TypeScript with full type safety
  • ✅ Vite-powered builds

The same extension functionality, dramatically better developer experience.


Understanding the .output Directory

WXT generates browser-specific builds in .output/:

.output/
├── chrome-mv3-dev/
│   ├── manifest.json       # Chrome-formatted manifest
│   ├── background.js       # Bundled background script
│   └── popup/
│       └── index.html
└── firefox-mv3-dev/
    ├── manifest.json       # Firefox-formatted manifest
    └── ...

Development Mode: Files include source maps and dev tooling

Production Mode: Run bun run build for optimized, minified bundles


Troubleshooting

Extension Doesn’t Appear

If the extension disappears from the dev Chrome window:

  1. Press Ctrl+C to stop dev server
  2. Run bun dev to restart
  3. Extension should reappear

This occasionally happens with manifest/config changes.

TypeScript Errors

WXT provides full type definitions for extension APIs:

// ❌ TypeScript error: Property 'url' might be undefined
const url = tab.url.toLowerCase();

// ✅ Safe with optional chaining
const url = tab.url?.toLowerCase() ?? '';

Trust TypeScript’s type checking - it prevents runtime errors.

Hot Reload Not Working

If changes don’t reflect automatically:

  • ✅ HTML/CSS/basic JS: Should hot reload
  • ❌ Config/manifest changes: Restart dev server
  • ❌ Background script: Restart dev server

What’s Next

You now have a modern development environment with hot reloading and cross-browser support. The extension works identically to Lesson 2 but with dramatically improved tooling.

Lesson 4 dives deep into manifest configuration - understanding permissions, host permissions, and the security model that governs what your extension can access.

From this point forward, all development uses WXT. The manual reload cycle is behind you.