Skip to content

WXT Template

The WXT template provides a modern browser extension development framework with Vue 3, TypeScript, and Vite, perfect for building cross-browser extensions with excellent developer experience.

Technical Stack

  • WXT - Browser extension development framework
  • Vue 3 - Progressive JavaScript framework
  • TypeScript - Type-safe development
  • Vite - Fast build tool and dev server
  • Vue Router - Client-side routing
  • Pinia - State management library
  • Vue i18n - Internationalization
  • Tailwind CSS - Utility-first CSS framework

Quick Start

1. Create Project

bash
# Initialize project
vup init my-extension-project
cd my-extension-project

# Add WXT template
vup add my-extension

2. Install Dependencies

bash
# Install dependencies
pnpm install

3. Start Development

bash
# Start development server
cd apps/my-extension
pnpm dev

The extension will be available in the browser with hot reload support.

Project Structure

apps/my-extension/
├── src/
│   ├── assets/           # Static assets
│   │   └── images/       # Image files
│   ├── components/       # Vue components
│   │   └── HelloWorld.vue # Demo component
│   ├── composables/      # Vue composables
│   ├── entrypoints/      # Extension entry points
│   │   ├── background.ts # Background script
│   │   ├── content.ts    # Content script
│   │   ├── newtab/       # New tab page
│   │   │   ├── newtab.html
│   │   │   └── newtab.vue
│   │   ├── options/      # Options page
│   │   │   ├── options.html
│   │   │   └── options.vue
│   │   └── popup/        # Popup page
│   │       ├── popup.html
│   │       └── popup.vue
│   ├── manifest.ts       # Extension manifest
│   └── vue-shim.d.ts     # Vue type declarations
├── public/               # Public assets
│   └── icon/             # Extension icons
│       ├── 16.png
│       ├── 32.png
│       ├── 48.png
│       ├── 96.png
│       └── 128.png
├── package.json          # Dependencies and scripts
├── tsconfig.json         # TypeScript configuration
└── wxt.config.ts         # WXT configuration

Core Features

Multiple Entry Points

Background Script

typescript
// entrypoints/background.ts
import { defineBackground } from 'wxt/sandbox';

export default defineBackground(() => {
  console.log('Background script loaded');

  // Listen for extension installation
  chrome.runtime.onInstalled.addListener((details) => {
    console.log('Extension installed:', details);
  });

  // Listen for messages from content scripts
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    console.log('Message received:', message);
    sendResponse({ status: 'success' });
  });
});

Content Script

typescript
// entrypoints/content.ts
import { defineContentScript } from 'wxt/sandbox';

export default defineContentScript({
  matches: ['<all_urls>'],
  main() {
    console.log('Content script loaded');

    // Inject content into the page
    const div = document.createElement('div');
    div.textContent = 'Hello from WXT!';
    div.style.cssText = `
      position: fixed;
      top: 10px;
      right: 10px;
      background: #007aff;
      color: white;
      padding: 10px;
      border-radius: 5px;
      z-index: 10000;
    `;
    document.body.appendChild(div);
  },
});
vue
<!-- entrypoints/popup/popup.vue -->
<template>
  <div class="popup-container">
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
    <button @click="handleClick" class="btn">
      {{ buttonText }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const title = ref('WXT Extension');
const description = ref('A modern browser extension');
const buttonText = ref('Click Me');

const handleClick = () => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (tabs[0]?.id) {
      chrome.tabs.sendMessage(tabs[0].id, { action: 'hello' });
    }
  });
};
</script>

<style scoped>
.popup-container {
  @apply w-80 p-4 bg-white;
}

.btn {
  @apply w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
</style>

Options Page

vue
<!-- entrypoints/options/options.vue -->
<template>
  <div class="options-container">
    <h1>Extension Settings</h1>
    <form @submit.prevent="saveSettings">
      <div class="form-group">
        <label for="theme">Theme:</label>
        <select v-model="settings.theme" id="theme">
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </div>
      <div class="form-group">
        <label for="notifications">Enable Notifications:</label>
        <input
          v-model="settings.notifications"
          type="checkbox"
          id="notifications"
        />
      </div>
      <button type="submit" class="btn">Save Settings</button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

interface Settings {
  theme: string;
  notifications: boolean;
}

const settings = ref<Settings>({
  theme: 'light',
  notifications: true,
});

onMounted(() => {
  // Load settings from storage
  chrome.storage.sync.get(['theme', 'notifications'], (result) => {
    settings.value = {
      theme: result.theme || 'light',
      notifications: result.notifications !== false,
    };
  });
});

const saveSettings = () => {
  chrome.storage.sync.set(settings.value, () => {
    console.log('Settings saved');
  });
};
</script>

<style scoped>
.options-container {
  @apply max-w-2xl mx-auto p-6;
}

.form-group {
  @apply mb-4;
}

.form-group label {
  @apply block mb-2 font-medium;
}

.form-group select,
.form-group input[type='checkbox'] {
  @apply w-full p-2 border rounded;
}

.btn {
  @apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
</style>

New Tab Page

vue
<!-- entrypoints/newtab/newtab.vue -->
<template>
  <div class="newtab-container">
    <div class="hero">
      <h1>{{ title }}</h1>
      <p>{{ description }}</p>
    </div>
    <div class="features">
      <div v-for="feature in features" :key="feature.title" class="feature">
        <h3>{{ feature.title }}</h3>
        <p>{{ feature.description }}</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const title = ref('WXT Extension');
const description = ref('A modern browser extension with Vue 3');

const features = ref([
  {
    title: 'Modern Framework',
    description: 'Built with Vue 3 and TypeScript',
  },
  {
    title: 'Fast Development',
    description: 'Hot reload and instant feedback',
  },
  {
    title: 'Cross-browser',
    description: 'Works on Chrome, Firefox, and Edge',
  },
]);
</script>

<style scoped>
.newtab-container {
  @apply min-h-screen bg-gray-100 p-8;
}

.hero {
  @apply text-center mb-12;
}

.hero h1 {
  @apply text-4xl font-bold text-gray-900 mb-4;
}

.hero p {
  @apply text-xl text-gray-600;
}

.features {
  @apply grid grid-cols-1 md:grid-cols-3 gap-8;
}

.feature {
  @apply bg-white p-6 rounded-lg shadow-md;
}

.feature h3 {
  @apply text-xl font-semibold text-gray-900 mb-2;
}

.feature p {
  @apply text-gray-600;
}
</style>

Extension Manifest

typescript
// manifest.ts
import { defineManifest } from 'wxt/sandbox';

export default defineManifest({
  manifest_version: 3,
  name: 'WXT Extension',
  version: '1.0.0',
  description: 'A modern browser extension built with WXT',
  permissions: ['storage', 'tabs', 'activeTab'],
  host_permissions: ['<all_urls>'],
  action: {
    default_popup: 'popup.html',
    default_title: 'WXT Extension',
  },
  background: {
    service_worker: 'background.js',
  },
  content_scripts: [
    {
      matches: ['<all_urls>'],
      js: ['content.js'],
    },
  ],
  web_accessible_resources: [
    {
      resources: ['icon/*.png'],
      matches: ['<all_urls>'],
    },
  ],
  icons: {
    16: 'icon/16.png',
    32: 'icon/32.png',
    48: 'icon/48.png',
    96: 'icon/96.png',
    128: 'icon/128.png',
  },
});

Development Tools

WXT Configuration

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

export default defineConfig({
  manifest: {
    name: 'WXT Extension',
    version: '1.0.0',
    description: 'A modern browser extension built with WXT',
  },
  modules: ['vue'],
  vue: {
    // Vue-specific configuration
  },
  runner: {
    // Development server configuration
    startUrls: ['https://example.com'],
  },
  build: {
    // Build configuration
    outDir: '.output',
  },
});

TypeScript Configuration

json
// tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./.output",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@shared/*": ["../../_shared/*"]
    }
  },
  "include": [
    "src/**/*",
    "src/**/*.vue",
    "src/*.vue",
    "./auto-imports.d.ts",
    "src/vue-shim.d.ts"
  ]
}

Available Scripts

json
{
  "scripts": {
    "dev": "wxt dev",
    "build": "wxt build",
    "build:firefox": "wxt build --browser firefox",
    "build:chrome": "wxt build --browser chrome",
    "build:edge": "wxt build --browser edge",
    "preview": "wxt preview",
    "zip": "wxt zip",
    "lint": "eslint src/ --ext .vue,.ts,.js",
    "lint:fix": "eslint src/ --ext .vue,.ts,.js --fix",
    "format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss}\"",
    "format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss}\""
  }
}

Best Practices

Message Passing

typescript
// utils/messaging.ts
export class MessageService {
  static async sendMessage<T = any>(message: any, tabId?: number): Promise<T> {
    return new Promise((resolve, reject) => {
      const target = tabId
        ? chrome.tabs.sendMessage
        : chrome.runtime.sendMessage;

      target(tabId || undefined, message, (response) => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError);
        } else {
          resolve(response);
        }
      });
    });
  }

  static onMessage<T = any>(
    callback: (message: T, sender: chrome.runtime.MessageSender) => void
  ) {
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      callback(message, sender);
      sendResponse({ status: 'success' });
    });
  }
}

Storage Management

typescript
// utils/storage.ts
export class StorageService {
  static async get<T = any>(key: string): Promise<T | null> {
    return new Promise((resolve) => {
      chrome.storage.sync.get([key], (result) => {
        resolve(result[key] || null);
      });
    });
  }

  static async set(key: string, value: any): Promise<void> {
    return new Promise((resolve) => {
      chrome.storage.sync.set({ [key]: value }, () => {
        resolve();
      });
    });
  }

  static async remove(key: string): Promise<void> {
    return new Promise((resolve) => {
      chrome.storage.sync.remove([key], () => {
        resolve();
      });
    });
  }
}

Error Handling

typescript
// utils/error.ts
export class ExtensionError extends Error {
  constructor(
    message: string,
    public code: string
  ) {
    super(message);
    this.name = 'ExtensionError';
  }
}

export function handleError(error: unknown): void {
  if (error instanceof ExtensionError) {
    console.error(`Extension Error [${error.code}]:`, error.message);
  } else if (error instanceof Error) {
    console.error('Unexpected Error:', error.message);
  } else {
    console.error('Unknown Error:', error);
  }
}

Build and Deployment

Development Build

bash
# Build for development
pnpm build

# The built files will be in the .output directory

Production Build

bash
# Build for specific browser
pnpm build:chrome
pnpm build:firefox
pnpm build:edge

# Create ZIP package
pnpm zip

Browser Installation

  1. Chrome/Edge: Load unpacked extension from .output directory
  2. Firefox: Load temporary add-on from .output directory
  3. Production: Upload ZIP file to respective web stores