WXT

wxt.dev
Developer Tools

WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.

llms.txt

url: / title: Next-gen Web Extension Framework

WXTNext-gen Web Extension Framework

An open source tool that makes web extension development faster than ever before.

Get Started

Learn More

WXT

🌐Supported BrowsersWXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.Read docs

✅MV2 and MV3Build Manifest V2 or V3 extensions for any browser using the same codebase.Read docs

Fast Dev Mode

Lightning fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.

📂File Based EntrypointsManifest is generated based on files in the project with inline configuration.See project structure

🚔

TypeScript

Create large projects with confidence using TS by default.

🦾Auto-importsNuxt-like auto-imports to speed up development.Read docs

🤖

Automated Publishing

Automatically zip, upload, submit, and publish extensions.

🎨Frontend Framework AgnosticWorks with any front-end framework with a Vite plugin.Add a framework

📦Module SystemReuse build-time and runtime-code across multiple extensions.Read docs

🖍️Bootstrap a New ProjectGet started quickly with several awesome project templates.See templates

📏

Bundle Analysis

Tools for analyzing the final extension bundle and minimizing your extension's size.

⬇️Bundle Remote CodeDownloads and bundles remote code imported from URLs.Read docs

Are you an LLM? View /llms.txt for optimized Markdown documentation, or /llms-full.txt for full documentation bundle

Sponsors

WXT is a MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider sponsoring WXT's development.

WXT Sponsors

Put Developer Experience First

WXT simplifies the web extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.

And who doesn't appreciate a beautiful CLI?

Who's Using WXT?

Battle tested and ready for production. Explore web extensions made with WXT.


url: /guide/introduction.html title: Welcome to WXT

Are you an LLM? You can read better optimized documentation at /guide/introduction.md for this page in Markdown format

Welcome to WXT

WXT is a modern, open-source framework for building web extensions. Inspired by Nuxt, its goals are to:

  • Provide an awesome DX
  • Provide first-class support for all major browsers

Check out the comparison to see how WXT compares to other tools for building web extensions.

Prerequisites

These docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs.

:::warning New to extension development? If you have never written an extension before, follow Chrome's Hello World tutorial to first create an extension without WXT, then come back here. :::

You should also be aware of Chrome's extension docs and Mozilla's extension docs. WXT does not change how you use the extension APIs, and you'll need to refer to these docs often when using specific APIs.


Alright, got a basic understanding of how web extensions are structured? Do you know how to access the extension APIs? Then continue to the Installation page to create your first WXT extension.


url: /guide/installation.html title: Installation

Are you an LLM? You can read better optimized documentation at /guide/installation.md for this page in Markdown format

Installation

Bootstrap a new project, start from scratch, or migrate an existing project.

Bootstrap Project

Run the init command, and follow the instructions.

:::code-group

pnpm dlx wxt@latest init
bunx wxt@latest init
npx wxt@latest init
# Use NPM initially, but select Yarn when prompted
npx wxt@latest init

:::

:::info Starter Templates: TypeScript LogoVanilla
Vue LogoVue
React LogoReact
Svelte LogoSvelte
Solid LogoSolid

All templates use TypeScript by default. To use JavaScript, change the file extensions. :::

Demo

wxt init demo

Once you've run the dev command, continue to Next Steps!

From Scratch

  1. Create a new project
    :::code-group
cd my-project  
pnpm init  
cd my-project  
bun init  
cd my-project  
npm init  
cd my-project  
yarn init  

::: 2. Install WXT:
:::code-group

pnpm i -D wxt  
bun add -D wxt  
npm i -D wxt  
yarn add --dev wxt  

::: 3. Add an entrypoint, my-project/entrypoints/background.ts:
:::code-group

export default defineBackground(() => {  
  console.log('Hello world!');  
});  

::: 4. Add scripts to your package.json:
package.json

{  
  "scripts": {  
    "dev": "wxt",  
    "dev:firefox": "wxt -b firefox",  
    "build": "wxt build",  
    "build:firefox": "wxt build -b firefox",  
    "zip": "wxt zip",  
    "zip:firefox": "wxt zip -b firefox",  
    "postinstall": "wxt prepare"  
  }  
}  
  1. Run your extension in dev mode
    :::code-group
pnpm dev  
bun run dev  
npm run dev  
yarn dev  

:::
WXT will automatically open a browser window with your extension installed.

Next Steps


url: /guide/essentials/project-structure.html title: Project Structure

Are you an LLM? You can read better optimized documentation at /guide/essentials/project-structure.md for this page in Markdown format

Project Structure

WXT follows a strict project structure. By default, it's a flat folder structure that looks like this:

📂 {rootDir}/
   📁 .output/
   📁 .wxt/
   📁 assets/
   📁 components/
   📁 composables/
   📁 entrypoints/
   📁 hooks/
   📁 modules/
   📁 public/
   📁 utils/
   📄 .env
   📄 .env.publish
   📄 app.config.ts
   📄 package.json
   📄 tsconfig.json
   📄 web-ext.config.ts
   📄 wxt.config.ts

Here's a brief summary of each of these files and directories:

  • .output/: All build artifacts will go here
  • .wxt/: Generated by WXT, it contains TS config
  • assets/: Contains all CSS, images, and other assets that should be processed by WXT
  • components/: Auto-imported by default, contains UI components
  • composables/: Auto-imported by default, contains source code for your project's composable functions for Vue
  • entrypoints/: Contains all the entrypoints that get bundled into your extension
  • hooks/: Auto-imported by default, contains source code for your project's hooks for React and Solid
  • modules/: Contains local WXT Modules for your project
  • public/: Contains any files you want to copy into the output folder as-is, without being processed by WXT
  • utils/: Auto-imported by default, contains generic utilities used throughout your project
  • .env: Contains Environment Variables
  • .env.publish: Contains Environment Variables for publishing
  • app.config.ts: Contains Runtime Config
  • package.json: The standard file used by your package manager
  • tsconfig.json: Config telling TypeScript how to behave
  • web-ext.config.ts: Configure Browser Startup
  • wxt.config.ts: The main config file for WXT projects

Adding a src/ Directory

Many developers like having a src/ directory to separate source code from configuration files. You can enable it inside the wxt.config.ts file:

wxt.config.ts

export default defineConfig({
  srcDir: 'src',
});

After enabling it, your project structure should look like this:

📂 {rootDir}/
   📁 .output/
   📁 .wxt/
   📁 modules/
   📁 public/
   📂 src/
      📁 assets/
      📁 components/
      📁 composables/
      📁 entrypoints/
      📁 hooks/
      📁 utils/
      📄 app.config.ts
   📄 .env
   📄 .env.publish
   📄 package.json
   📄 tsconfig.json
   📄 web-ext.config.ts
   📄 wxt.config.ts

Customizing Other Directories

You can configure the following directories:

wxt.config.ts

export default defineConfig({
  // Relative to project root
  srcDir: "src",             // default: "."
  modulesDir: "wxt-modules", // default: "modules"
  outDir: "dist",            // default: ".output"
  publicDir: "static",       // default: "public"

  // Relative to srcDir
  entrypointsDir: "entries", // default: "entrypoints"
})

You can use absolute or relative paths.


url: /guide/essentials/entrypoints.html title: Entrypoints

Are you an LLM? You can read better optimized documentation at /guide/essentials/entrypoints.md for this page in Markdown format

Entrypoints

WXT uses the files inside the entrypoints/ directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (TS, JSX, SCSS, etc).

Folder Structure

Inside the entrypoints/ directory, an entrypoint is defined as a single file or directory (with an index file) inside it.

:::code-group

📂 entrypoints/
   📄 {name}.{ext}
📂 entrypoints/
   📂 {name}/
      📄 index.{ext}

:::

The entrypoint's name dictates the type of entrypoint. For example, to add a "Background" entrypoint, either of these files would work:

:::code-group

📂 entrypoints/
   📄 background.ts
📂 entrypoints/
   📂 background/
      📄 index.ts

:::

Refer to the Entrypoint Types section for the full list of listed entrypoints and their filename patterns.

Including Other Files

When using an entrypoint directory, entrypoints/{name}/index.{ext}, you can add related files next to the index file.

📂 entrypoints/
   📂 popup/
      📄 index.html     ← This file is the entrypoint
      📄 main.ts
      📄 style.css
   📂 background/
      📄 index.ts       ← This file is the entrypoint
      📄 alarms.ts
      📄 messaging.ts
   📂 youtube.content/
      📄 index.ts       ← This file is the entrypoint
      📄 style.css

:::danger DO NOT put files related to an entrypoint directly inside the entrypoints/ directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error.

Instead, use a directory for that entrypoint:

📂 entrypoints/
   📄 popup.html 
   📄 popup.ts 
   📄 popup.css 
   📂 popup/ 
      📄 index.html 
      📄 main.ts 
      📄 style.css 

:::

Deeply Nested Entrypoints

While the entrypoints/ directory might resemble the pages/ directory of other web frameworks, like Nuxt or Next.js, it does not support deeply nesting entrypoints in the same way.

Entrypoints must be zero or one levels deep for WXT to discover and build them:

📂 entrypoints/
   📂 youtube/ 
       📂 content/ 
          📄 index.ts 
          📄 ... 
       📂 injected/ 
          📄 index.ts 
          📄 ... 
   📂 youtube.content/ 
      📄 index.ts 
      📄 ... 
   📂 youtube-injected/ 
      📄 index.ts 
      📄 ... 

Unlisted Entrypoints

In web extensions, there are two types of entrypoints:

  1. Listed: Referenced in the manifest.json
  2. Unlisted: Not referenced in the manifest.json

Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:

  • Popup
  • Options
  • Background
  • Content Script

However, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example:

  • A welcome page shown in a new tab when the extension is installed
  • JS files injected by content scripts into the main world

For more details on how to add unlisted entrypoints, see:

Defining Manifest Options

Most listed entrypoints have options that need to be added to the manifest.json. However with WXT, instead of defining the options in a separate file, you define these options inside the entrypoint file itself.

For example, here's how to define matches for content scripts:

entrypoints/content.ts

export default defineContentScript({
  matches: ['*://*.wxt.dev/*'],
  main() {
    // ...
  },
});

For HTML entrypoints, options are configured as <meta> tags. For example, to use a page_action for your MV2 popup:

<!doctype html>
<html lang="en">
  <head>
    <meta name="manifest.type" content="page_action" />
  </head>
</html>

Refer to the Entrypoint Types sections for a list of options configurable inside each entrypoint, and how to define them.

When building your extension, WXT will look at the options defined in your entrypoints, and generate the manifest accordingly.

Entrypoint Types

Background

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/background.[jt]s/background.js
entrypoints/background/index.[jt]s/background.js

:::code-group

export default defineBackground(() => {
  // Executed when background is loaded
});
export default defineBackground({
  // Set manifest options
  persistent: undefined | true | false,
  type: undefined | 'module',

  // Set include/exclude if the background should be removed from some builds
  include: undefined | string[],
  exclude: undefined | string[],

  main() {
    // Executed when background is loaded, CANNOT BE ASYNC
  },
});

:::

For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.

When defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the main function.

browser.action.onClicked.addListener(() => { 
  // ...
}); 

export default defineBackground(() => {
  browser.action.onClicked.addListener(() => { 
    // ...
  }); 
});

Refer to the Entrypoint Loaders documentation for more details.

Bookmarks

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/bookmarks.html/bookmarks.html
entrypoints/bookmarks/index.html/bookmarks.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

When you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page.

Content Scripts

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/content.[jt]sx?/content-scripts/content.js
entrypoints/content/index.[jt]sx?/content-scripts/content.js
entrypoints/{name}.content.[jt]sx?/content-scripts/{name}.js
entrypoints/{name}.content/index.[jt]sx?/content-scripts/{name}.js
export default defineContentScript({
  // Set manifest options
  matches: string[],
  excludeMatches: undefined | [],
  includeGlobs: undefined | [],
  excludeGlobs: undefined | [],
  allFrames: undefined | true | false,
  runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
  matchAboutBlank: undefined | true | false,
  matchOriginAsFallback: undefined | true | false,
  world: undefined | 'ISOLATED' | 'MAIN',

  // Set include/exclude if the background should be removed from some builds
  include: undefined | string[],
  exclude: undefined | string[],

  // Configure how CSS is injected onto the page
  cssInjectionMode: undefined | "manifest" | "manual" | "ui",

  // Configure how/when content script will be registered
  registration: undefined | "manifest" | "runtime",

  main(ctx: ContentScriptContext) {
    // Executed when content script is loaded, can be async
  },
});

When defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the main function.

const container = document.createElement('div'); 
document.body.append(container); 

export default defineContentScript({
  main: function () {
    const container = document.createElement('div'); 
    document.body.append(container); 
  },
});

Refer to the Entrypoint Loaders documentation for more details.

See Content Script UI for more info on creating UIs and including CSS in content scripts.

Devtools

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/devtools.html/devtools.html
entrypoints/devtools/index.html/devtools.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

Follow the Devtools Example to add different panels and panes.

History

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/history.html/history.html
entrypoints/history/index.html/history.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

When you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page.

Newtab

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/newtab.html/newtab.html
entrypoints/newtab/index.html/newtab.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

When you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page.

Options

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/options.html/options.html
entrypoints/options/index.html/options.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Options Title</title>

    <!-- Customize the manifest options -->
    <meta name="manifest.open_in_tab" content="true|false" />
    <meta name="manifest.chrome_style" content="true|false" />
    <meta name="manifest.browser_style" content="true|false" />

    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

Popup

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/popup.html/popup.html
entrypoints/popup/index.html/popup.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <!-- Set the `action.default_title` in the manifest -->
    <title>Default Popup Title</title>

    <!-- Customize the manifest options -->
    <meta
      name="manifest.default_icon"
      content="{
        '16': '/icon-16.png',
        '24': '/icon-24.png',
        ...
      }"
    />
    <meta name="manifest.type" content="page_action|browser_action" />
    <meta name="manifest.browser_style" content="true|false" />
    <!-- Firefox only: where to place the action button -->
    <meta
      name="manifest.default_area"
      content="navbar|menupanel|tabstrip|personaltoolbar"
    />
    <!-- Firefox only: icons for light/dark themes -->
    <meta
      name="manifest.theme_icons"
      content="[
        { light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
        { light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }
      ]"
    />

    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

Sandbox

Chrome Docs

:::warning Chromium Only Firefox does not support sandboxed pages. :::

FilenameOutput Path
entrypoints/sandbox.html/sandbox.html
entrypoints/sandbox/index.html/sandbox.html
entrypoints/{name}.sandbox.html/{name}.html
entrypoints/{name}.sandbox/index.html/{name}.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>

    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

Side Panel

Chrome DocsFirefox Docs

FilenameOutput Path
entrypoints/sidepanel.html/sidepanel.html
entrypoints/sidepanel/index.html/sidepanel.html
entrypoints/{name}.sidepanel.html/{name}.html`
entrypoints/{name}.sidepanel/index.html/{name}.html`
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Default Side Panel Title</title>

    <!-- Customize the manifest options -->
    <meta
      name="manifest.default_icon"
      content="{
        '16': '/icon-16.png',
        '24': '/icon-24.png',
        ...
      }"
    />
    <meta name="manifest.open_at_install" content="true|false" />
    <meta name="manifest.browser_style" content="true|false" />

    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

In Chrome, side panels use the side_panel API, while Firefox uses the sidebar_action API.

Unlisted CSS

FilenameOutput Path
entrypoints/{name}.(css|scsssass
entrypoints/{name}/index.(css|scsssass
entrypoints/content.(css|scsssass
entrypoints/content/index.(css|scsssass
entrypoints/{name}.content.(css|scsssass
entrypoints/{name}.content/index.(css|scsssass
body {
  /* ... */
}

Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors

CSS entrypoints are always unlisted. To add CSS to a content script, see the Content Script docs.

Unlisted Pages

FilenameOutput Path
entrypoints/{name}.html/{name}.html
entrypoints/{name}/index.html/{name}.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>

    <!-- Set include/exclude if the page should be removed from some builds -->
    <meta name="manifest.include" content="['chrome', ...]" />
    <meta name="manifest.exclude" content="['chrome', ...]" />
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

At runtime, unlisted pages are accessible at /{name}.html:

const url = browser.runtime.getURL('/{name}.html');

console.log(url); // "chrome-extension://{id}/{name}.html"
window.open(url); // Open the page in a new tab

Unlisted Scripts

FilenameOutput Path
entrypoints/{name}.[jt]sx?/{name}.js
entrypoints/{name}/index.[jt]sx?/{name}.js

:::code-group

export default defineUnlistedScript(() => {
  // Executed when script is loaded
});
export default defineUnlistedScript({
  // Set include/exclude if the script should be removed from some builds
  include: undefined | string[],
  exclude: undefined | string[],

  main() {
    // Executed when script is loaded
  },
});

:::

At runtime, unlisted scripts are accessible from /{name}.js:

const url = browser.runtime.getURL('/{name}.js');

console.log(url); // "chrome-extension://{id}/{name}.js"

You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to web_accessible_resources.

When defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the main function.

document.querySelectorAll('a').forEach((anchor) => { 
  // ...
}); 

export default defineUnlistedScript(() => {
  document.querySelectorAll('a').forEach((anchor) => { 
    // ...
  }); 
});

Refer to the Entrypoint Loaders documentation for more details.


url: /guide/essentials/config/manifest.html title: Manifest

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/manifest.md for this page in Markdown format

Manifest

In WXT, there is no manifest.json file in your source code. Instead, WXT generates the manifest from multiple sources:

Your extension's manifest.json will be output to .output/{target}/manifest.json when running wxt build.

Global Options

To add a property to your manifest, use the manifest config inside your wxt.config.ts:

export default defineConfig({
  manifest: {
    // Put manual changes here
  },
});

You can also define the manifest as a function, and use JS to generate it based on the target browser, mode, and more.

export default defineConfig({
  manifest: ({ browser, manifestVersion, mode, command }) => {
    return {
      // ...
    };
  },
});

MV2 and MV3 Compatibility

When adding properties to the manifest, always define the property in it's MV3 format when possible. When targeting MV2, WXT will automatically convert these properties to their MV2 format.

For example, for this config:

export default defineConfig({
  manifest: {
    action: {
      default_title: 'Some Title',
    },
    web_accessible_resources: [
      {
        matches: ['*://*.google.com/*'],
        resources: ['icon/*.png'],
      },
    ],
  },
});

WXT will generate the following manifests:

:::code-group

{
  "manifest_version": 2,
  // ...
  "browser_action": {
    "default_title": "Some Title"
  },
  "web_accessible_resources": ["icon/*.png"]
}
{
  "manifest_version": 3,
  // ...
  "action": {
    "default_title": "Some Title"
  },
  "web_accessible_resources": [
    {
      "matches": ["*://*.google.com/*"],
      "resources": ["icon/*.png"]
    }
  ]
}

:::

You can also specify properties specific to a single manifest version, and they will be stripped out when targeting the other manifest version.

Name

Chrome Docs

If not provided via the manifest config, the manifest's name property defaults to your package.json's name property.

Version and Version Name

Chrome Docs

Your extension's version and version_name is based on the version from your package.json.

  • version_name is the exact string listed
  • version is the string cleaned up, with any invalid suffixes removed

Example:

// package.json
{
  "version": "1.3.0-alpha2"
}
// .output/<target>/manifest.json
{
  "version": "1.3.0",
  "version_name": "1.3.0-alpha2"
}

If a version is not present in your package.json, it defaults to "0.0.0".

Icons

WXT automatically discovers your extension's icon by looking at files in the public/ directory:

public/
├─ icon-16.png
├─ icon-24.png
├─ icon-48.png
├─ icon-96.png
└─ icon-128.png

Specifically, an icon must match one of these regex to be discovered:

const iconRegex = [
  /^icon-([0-9]+)\.png$/,                 // icon-16.png
  /^icon-([0-9]+)x[0-9]+\.png$/,          // icon-16x16.png
  /^icon@([0-9]+)w\.png$/,                // icon@16w.png
  /^icon@([0-9]+)h\.png$/,                // icon@16h.png
  /^icon@([0-9]+)\.png$/,                 // icon@16.png
  /^icons?[/\\]([0-9]+)\.png$/,           // icon/16.png | icons/16.png
  /^icons?[/\\]([0-9]+)x[0-9]+\.png$/,    // icon/16x16.png | icons/16x16.png
];

If you don't like these filename or you're migrating to WXT and don't want to rename the files, you can manually specify an icon in your manifest:

export default defineConfig({
  manifest: {
    icons: {
      16: '/extension-icon-16.png',
      24: '/extension-icon-24.png',
      48: '/extension-icon-48.png',
      96: '/extension-icon-96.png',
      128: '/extension-icon-128.png',
    },
  },
});

Alternatively, you can use @wxt-dev/auto-icons to let WXT generate your icon at the required sizes.

Firefox theme_icons

Firefox supports a theme_icons field on the toolbar action that swaps between a light and dark variant based on the current browser theme.

When targeting Firefox, WXT auto-discovers paired light/dark icons in the public/ directory and attaches them to action (MV3) or browser_action (MV2). You only need to drop both variants next to your regular icons:

public/
├─ icon-16.png         # regular (default_icon)
├─ icon-light-16.png   # Firefox light theme
├─ icon-dark-16.png    # Firefox dark theme
├─ icon-32.png
├─ icon-light-32.png
└─ icon-dark-32.png

A size is only included in theme_icons if both a light and a dark file are present. The following filename patterns are discovered:

const themeIconRegex = [
  /^icon-(?<variant>light|dark)-(?<size>[0-9]+)\.png$/,              // icon-light-16.png
  /^icon-(?<variant>light|dark)-(?<size>[0-9]+)x[0-9]+\.png$/,       // icon-light-16x16.png
  /^icon-(?<size>[0-9]+)-(?<variant>light|dark)\.png$/,              // icon-16-light.png
  /^icon-(?<size>[0-9]+)x[0-9]+-(?<variant>light|dark)\.png$/,       // icon-16x16-light.png
  /^icons?[/\\](?<variant>light|dark)-(?<size>[0-9]+)\.png$/,        // icon/light-16.png | icons/light-16.png
  /^icons?[/\\](?<variant>light|dark)-(?<size>[0-9]+)x[0-9]+\.png$/, // icon/light-16x16.png
  /^icons?[/\\](?<size>[0-9]+)-(?<variant>light|dark)\.png$/,        // icon/16-light.png
  /^icons?[/\\](?<size>[0-9]+)x[0-9]+-(?<variant>light|dark)\.png$/, // icon/16x16-light.png
];

Only .png files are discovered today, even though Firefox supports .svg - follow #1120 for updates.

If you set manifest.action.theme_icons (or manifest.browser_action.theme_icons) explicitly in wxt.config.ts, WXT will not overwrite it.

Permissions

Chrome docs

Most of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically:

  • During development: the tabs and scripting permissions will be added to enable hot reloading.
  • When a sidepanel entrypoint is present: The sidepanel permission is added.
export default defineConfig({
  manifest: {
    permissions: ['storage', 'tabs'],
  },
});

:::warning Different browsers support different permissions. You are responsible for passing only the permissions required for each browser:

export default defineConfig({
  manifest: ({ browser }) => ({
    permissions:
      browser === 'chrome'
        ? ['storage', 'favicon', 'declarativeNetRequest']
        : ['storage', 'webRequest'],
  }),
});

:::

Host Permissions

Chrome docs

export default defineConfig({
  manifest: {
    host_permissions: ['https://www.google.com/*'],
  },
});

:::warning If you use host permissions and target both MV2 and MV3, make sure to only include the required host permissions for each version:

export default defineConfig({
  manifest: ({ manifestVersion }) => ({
    host_permissions: manifestVersion === 2 ? [...] : [...],
  }),
});

:::

Default Locale

export default defineConfig({
  manifest: {
    name: '__MSG_extName__',
    description: '__MSG_extDescription__',
    default_locale: 'en',
  },
});

See I18n docs for a full guide on internationalizing your extension.

Actions

In MV2, you have two options: browser_action and page_action. In MV3, they were merged into a single action API.

By default, whenever an action is generated, WXT falls back to browser_action when targeting MV2.

Action With Popup

To generate a manifest where a UI appears after clicking the icon, just create a Popup entrypoint. If you want to use a page_action for MV2, add the following meta tag to the HTML document's head:

<meta name="manifest.type" content="page_action" />

Action Without Popup

If you want to use the activeTab permission or the browser.action.onClicked event, but don't want to show a popup:

  1. Delete the Popup entrypoint if it exists
  2. Add the action key to your manifest:
export default defineConfig({  
  manifest: {  
    action: {},  
  },  
});  

Same as an action with a popup, WXT will fallback on using browser_action for MV2. To use a page_action instead, add that key as well:

export default defineConfig({
  manifest: {
    action: {},
    page_action: {},
  },
});

url: /guide/essentials/config/browser-startup.html title: Browser Startup

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/browser-startup.md for this page in Markdown format

Browser Startup

See the API Reference for a full list of config.

During development, WXT uses web-ext by Mozilla to automatically open a browser window with your extension installed.

Config Files

You can configure browser startup in 3 places:

  1. <rootDir>/web-ext.config.ts: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
    web-ext.config.ts
import { defineWebExtConfig } from 'wxt';  
export default defineWebExtConfig({  
  // ...  
});  
  1. <rootDir>/wxt.config.ts: Via the webExt config, included in version control
  2. $HOME/web-ext.config.ts: Provide default values for all WXT projects on your computer

Recipes

Set Browser Binaries

To set or customize the browser opened during development:

web-ext.config.ts

import { defineWebExtConfig } from 'wxt';

export default defineWebExtConfig({
  binaries: {
    chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
    firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
    edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
  },
});

wxt.config.ts

export default defineConfig({
  webExt: {
    binaries: {
      chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
      firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
      edge: '/path/to/edge', // Open MS Edge when running "wxt -b edge"
    },
  },
});

By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.

Persist Data

By default, to keep from modifying your browser's existing profiles, web-ext creates a brand new profile every time you run the dev script.

Right now, Chromium based browsers are the only browsers that support overriding this behavior and persisting data when running the dev script multiple times.

To persist data, set the --user-data-dir flag in any of the config files mentioned above:

:::code-group

import { defineWebExtConfig } from 'wxt';

export default defineWebExtConfig({
  chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
});
import { resolve } from 'node:path';
import { defineWebExtConfig } from 'wxt';

export default defineWebExtConfig({
  // On Windows, the path must be absolute
  chromiumProfile: resolve('.wxt/chrome-data'),
  keepProfileChanges: true,
});

:::

Now, next time you run the dev script, a persistent profile will be created in .wxt/chrome-data/{profile-name}. With a persistent profile, you can install devtools extensions to help with development, allow the browser to remember logins, etc, without worrying about the profile being reset the next time you run the dev script.

:::tip You can use any directory you'd like for --user-data-dir, the examples above create a persistent profile for each WXT project. To create a profile for all WXT projects, you can put the chrome-data directory inside your user's home directory. :::

Disable Opening Browser

If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:

web-ext.config.ts

import { defineWebExtConfig } from 'wxt';

export default defineWebExtConfig({
  disabled: true,
});

Enabling Chrome Features

Some APIs are disabled in Chrome during development because of the default flags web-ext uses to launch Chrome, like the Prompt API.

If your extension depends on new features or services, you can enable them via chromiumArgs:

import { defineWebExtConfig } from 'wxt';

export default defineWebExtConfig({
  chromiumArgs: [
    // For example, this flag enables the Prompt API
    '--disable-features=DisableLoadExtensionCommandLineSwitch',
  ],
});

There is no comprehensive list of what feature flags enable what APIs and services.

Alternatively, if you can't find a flag that enables a feature you're looking for, disable the opening the browser during development and use your regular chrome profile for development.


url: /guide/essentials/config/auto-imports.html title: Auto-imports

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/auto-imports.md for this page in Markdown format

Auto-imports

WXT uses unimport, the same tool as Nuxt, to setup auto-imports.

export default defineConfig({
  // See https://www.npmjs.com/package/unimport#configurations
  imports: {
    // ...
  },
});

By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:

  • <srcDir>/components/*
  • <srcDir>/composables/*
  • <srcDir>/hooks/*
  • <srcDir>/utils/*

All named and default exports from files in these directories are available everywhere else in your project without having to import them.

To see the complete list of auto-imported APIs, run wxt prepare and look at your project's .wxt/types/imports-module.d.ts file.

TypeScript

For TypeScript and your editor to recognize auto-imported variables, you need to run the wxt prepare command.

Add this command to your postinstall script so your editor has everything it needs to report type errors after installing dependencies:

// package.json
{
  "scripts": {
    "postinstall": "wxt prepare", 
  },
}

ESLint

ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the ESLint's globals. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it.

:::code-group

export default defineConfig({
  imports: {
    eslintrc: {
      enabled: 9,
    },
  },
});
export default defineConfig({
  imports: {
    eslintrc: {
      enabled: 8,
    },
  },
});

:::

Then in your ESLint config, import and use the generated file:

:::code-group

// eslint.config.mjs
import autoImports from './.wxt/eslint-auto-imports.mjs';

export default [
  autoImports,
  {
    // The rest of your config...
  },
];
// .eslintrc.mjs
export default {
  extends: ['./.wxt/eslintrc-auto-import.json'],
  // The rest of your config...
};

:::

Disabling Auto-imports

Not all developers like auto-imports. To disable them, set imports to false.

export default defineConfig({
  imports: false, 
});

Explicit Imports (#imports)

You can manually import all of WXT's APIs via the #imports module:

import {
  createShadowRootUi,
  ContentScriptContext,
  MatchPattern,
} from '#imports';

To learn more about how the #imports module works, read the related blog post.

If you've disabled auto-imports, you should still use #imports to import all of WXT's APIs from a single place.


url: /guide/essentials/config/environment-variables.html title: Environment Variables

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/environment-variables.md for this page in Markdown format

Environment Variables

Dotenv Files

WXT supports dotenv files the same way as Vite. Create any of the following files:

.env
.env.local
.env.[mode]
.env.[mode].local
.env.[browser]
.env.[browser].local
.env.[mode].[browser]
.env.[mode].[browser].local

And any environment variables listed inside them will be available at runtime:

# .env
WXT_API_KEY=...
await fetch(`/some-api?apiKey=${import.meta.env.WXT_API_KEY}`);

Remember to prefix any environment variables with WXT_ or VITE_, otherwise they won't be available at runtime, as per Vite's convention.

Built-in Environment Variables

WXT provides some custom environment variables based on the current command:

UsageTypeDescription
import.meta.env.MANIFEST_VERSION2 │ 3The target manifest version
import.meta.env.BROWSERstringThe target browser
import.meta.env.CHROMEbooleanEquivalent to import.meta.env.BROWSER === "chrome"
import.meta.env.FIREFOXbooleanEquivalent to import.meta.env.BROWSER === "firefox"
import.meta.env.SAFARIbooleanEquivalent to import.meta.env.BROWSER === "safari"
import.meta.env.EDGEbooleanEquivalent to import.meta.env.BROWSER === "edge"
import.meta.env.OPERAbooleanEquivalent to import.meta.env.BROWSER === "opera"

You can set the targetBrowsers option to make the BROWSER variable a more specific type, like "chrome" | "firefox".

You can also access all of Vite's environment variables:

UsageTypeDescription
import.meta.env.MODEstringThe mode the extension is running in
import.meta.env.PRODbooleanWhen NODE_ENV='production'
import.meta.env.DEVbooleanOpposite of import.meta.env.PROD

:::details Other Vite Environment Variables Vite provides two other environment variables, but they aren't useful in WXT projects:

  • import.meta.env.BASE_URL: Use browser.runtime.getURL instead.
  • import.meta.env.SSR: Always false. :::

Manifest

To use environment variables in the manifest, you need to use the function syntax:

export default defineConfig({
  modules: ['@wxt-dev/module-vue'],
  manifest: { 
    oauth2: { 
      client_id: import.meta.env.WXT_APP_CLIENT_ID
    } 
  } 
  manifest: () => ({ 
    oauth2: { 
      client_id: import.meta.env.WXT_APP_CLIENT_ID
    } 
  }), 
});

WXT can't load your .env files until after the config file has been loaded. So by using the function syntax for manifest, it defers creating the object until after the .env files are loaded into the process.

Note that Vite's runtime environment variables, like import.meta.env.DEV, will not be defined. Instead, access the mode like this:

export default defineConfig({
  manifest: ({ mode }) => {
    const isDev = mode === 'development';
    console.log('Is development mode:', isDev);

    // ...
  },
});

url: /guide/essentials/config/runtime.html title: Runtime Config

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/runtime.md for this page in Markdown format

Runtime Config

This API is still a WIP, with more features coming soon!

Define runtime configuration in a single place, <srcDir>/app.config.ts:

import { defineAppConfig } from '#imports';

// Define types for your config
declare module 'wxt/utils/define-app-config' {
  export interface WxtAppConfig {
    theme?: 'light' | 'dark';
  }
}

export default defineAppConfig({
  theme: 'dark',
});

:::warning This file is committed to the repo, so don't put any secrets here. Instead, use Environment Variables :::

To access runtime config, WXT provides the getAppConfig function:

import { getAppConfig } from '#imports';

console.log(getAppConfig()); // { theme: "dark" }

Environment Variables in App Config

You can use environment variables in the app.config.ts file.

declare module 'wxt/utils/define-app-config' {
  export interface WxtAppConfig {
    apiKey?: string;
    skipWelcome: boolean;
  }
}

export default defineAppConfig({
  apiKey: import.meta.env.WXT_API_KEY,
  skipWelcome: import.meta.env.WXT_SKIP_WELCOME === 'true',
});

This has several advantages:

  • Define all expected environment variables in a single file
  • Convert strings to other types, like booleans or arrays
  • Provide default values if an environment variable is not provided

url: /guide/essentials/config/vite.html title: Vite

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/vite.md for this page in Markdown format

Vite

WXT uses Vite under the hood to bundle your extension.

This page explains how to customize your project's Vite config. Refer to Vite's documentation to learn more about configuring the bundler.

:::tip In most cases, you shouldn't change Vite's build settings. WXT provides sensible defaults that output a valid extension accepted by all stores when publishing. :::

Change Vite Config

You can change Vite's config via the wxt.config.ts file:

wxt.config.ts

import { defineConfig } from 'wxt';

export default defineConfig({
  vite: () => ({
    // Override config here, same as `defineConfig({ ... })`
    // inside vite.config.ts files
  }),
});

Add Vite Plugins

To add a plugin, install the NPM package and add it to the Vite config:

wxt.config.ts

import { defineConfig } from 'wxt';
import VueRouter from 'unplugin-vue-router/vite';

export default defineConfig({
  vite: () => ({
    plugins: [
      VueRouter({
        /* ... */
      }),
    ],
  }),
});

:::warning Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. For example, vite-plugin-remove-console normally only runs when you build for production (vite build). However, WXT uses a combination of dev server and builds during development, so you need to manually tell it when to run:

wxt.config.ts

import { defineConfig } from 'wxt';
import removeConsole from 'vite-plugin-remove-console';

export default defineConfig({
  vite: (configEnv) => ({
    plugins:
      configEnv.mode === 'production'
        ? [removeConsole({ includes: ['log'] })]
        : [],
  }),
});

Search GitHub issues if you run into issues with a specific plugin.

If an issue doesn't exist for your plugin, open a new one. :::


url: /guide/essentials/config/build-mode.html title: Build Modes

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/build-mode.md for this page in Markdown format

Build Modes

Because WXT is powered by Vite, it supports modes in the same way.

When running any dev or build commands, pass the --mode flag:

wxt --mode production
wxt build --mode development
wxt zip --mode testing

By default, --mode is development for the dev command and production for all other commands (build, zip, etc).

Get Mode at Runtime

You can access the current mode in your extension using import.meta.env.MODE:

switch (import.meta.env.MODE) {
  case 'development': // ...
  case 'production': // ...

  // Custom modes specified with --mode
  case 'testing': // ...
  case 'staging': // ...
  // ...
}

url: /guide/essentials/config/typescript.html title: TypeScript Configuration

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/typescript.md for this page in Markdown format

TypeScript Configuration

When you run wxt prepare, WXT generates a base TSConfig file for your project at <rootDir>/.wxt/tsconfig.json.

At a minimum, you need to create a TSConfig in your root directory that looks like this:

// <rootDir>/tsconfig.json
{
  "extends": ".wxt/tsconfig.json",
}

Or if you're in a monorepo, you may not want to extend the config. If you don't extend it, you need to add .wxt/wxt.d.ts to the TypeScript project:

/// <reference path="./.wxt/wxt.d.ts" />

Compiler Options

To specify custom compiler options, add them in <rootDir>/tsconfig.json:

// <rootDir>/tsconfig.json
{
  "extends": ".wxt/tsconfig.json",
  "compilerOptions": {
    "jsx": "preserve",
  },
}

TSConfig Paths

WXT provides a default set of path aliases.

AliasToExample
~~/*import "~~/scripts"
@@/*import "@@/scripts"
~/*import { toLowerCase } from "~/utils/strings"
@/*import { toLowerCase } from "@/utils/strings"

To add your own, DO NOT add them to your tsconfig.json! Instead, use the alias option in wxt.config.ts.

This will add your custom aliases to <rootDir>/.wxt/tsconfig.json next time you run wxt prepare. It also adds your alias to the bundler so it can resolve imports.

import { resolve } from 'node:path';

export default defineConfig({
  alias: {
    // Directory:
    testing: resolve('utils/testing'),
    // File:
    strings: resolve('utils/strings.ts'),
  },
});
import { fakeTab } from 'testing/fake-objects';
import { toLowerCase } from 'strings';

url: /guide/essentials/config/hooks.html title: Hooks

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/hooks.md for this page in Markdown format

Hooks

WXT includes a system that lets you hook into the build process and make changes.

Adding Hooks

The easiest way to add a hook is via the wxt.config.ts. Here's an example hook that modifies the manifest.json file before it is written to the output directory:

wxt.config.ts

export default defineConfig({
  hooks: {
    'build:manifestGenerated': (wxt, manifest) => {
      if (wxt.config.mode === 'development') {
        manifest.name += ' (DEV)';
      }
    },
  },
});

Most hooks provide the wxt object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.

You can find the list of all available hooks in the API reference.

Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into WXT Modules instead.

Execution Order

Because hooks can be defined in multiple places, including WXT Modules, the order which they're executed can matter. Hooks are executed in the following order:

  1. NPM modules in the order listed in the modules config
  2. User modules in /modules folder, loaded alphabetically
  3. Hooks listed in your wxt.config.ts

To see the order for your project, run wxt prepare --debug flag and search for the "Hook execution order":

⚙ Hook execution order:
⚙   1. wxt:built-in:unimport
⚙   2. src/modules/auto-icons.ts
⚙   3. src/modules/example.ts
⚙   4. src/modules/i18n.ts
⚙   5. wxt.config.ts > hooks

Changing execution order is simple:

  • Prefix your user modules with a number (lower numbers are loaded first):
📁 modules/  
   📄 0.my-module.ts  
   📄 1.another-module.ts  
  • If you need to run an NPM module after user modules, just make it a user module and prefix the filename with a number!
// modules/2.i18n.ts  
export { default } from '@wxt-dev/i18n/module';  

url: /guide/essentials/config/entrypoint-loaders.html title: Entrypoint Loaders

Are you an LLM? You can read better optimized documentation at /guide/essentials/config/entrypoint-loaders.md for this page in Markdown format

Entrypoint Loaders

To generate the manifest and other files at build-time, WXT must import each entrypoint to get their options, like content script matches. For HTML files, this is easy. For JS/TS entrypoints, the process is more complicated.

When loading your JS/TS entrypoints, they are imported into a NodeJS environment, not the browser environment that they normally run in. This can lead to issues commonly seen when running browser-only code in a NodeJS environment, like missing global variables.

WXT does several pre-processing steps to try and prevent errors during this process:

  1. Use linkedom to make a small set of browser globals (window, document, etc) available.
  2. Use @webext-core/fake-browser to create a fake version of the chrome and browser globals expected by extensions.
  3. Pre-process the JS/TS code, stripping out the main function then tree-shaking unused code from the file

However, this process is not perfect. It doesn't setup all the globals found in the browser and the APIs may behave differently. As such, you should avoid using browser or extension APIs outside the main function of your entrypoints! See Entrypoint Limitations for more details.

:::tip If you're running into errors while importing entrypoints, run wxt prepare --debug to see more details about this process. When debugging, WXT will print out the pre-processed code to help you identify issues. :::

Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.


url: /guide/essentials/extension-apis.html title: Extension APIs

Are you an LLM? You can read better optimized documentation at /guide/essentials/extension-apis.md for this page in Markdown format

Extension APIs

Chrome DocsFirefox Docs

Different browsers provide different global variables for accessing the extension APIs (chrome provides chrome, firefox provides browser, etc).

WXT merges these two into a unified API accessed through the browser variable.

import { browser } from 'wxt/browser';

browser.action.onClicked.addListener(() => {
  // ...
});

:::tip With auto-imports enabled, you don't even need to import this variable from wxt/browser! :::

The browser variable WXT provides is a simple export of the browser or chrome globals provided by the browser at runtime:

export const browser = globalThis.browser?.runtime?.id
  ? globalThis.browser
  : globalThis.chrome;

This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).

Accessing Types

All types can be accessed via WXT's Browser namespace:

import { type Browser } from 'wxt/browser';

function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
  // ...
}

Using webextension-polyfill

If you want to use the webextension-polyfill when importing browser, you can do so by installing the @wxt-dev/webextension-polyfill package.

See it's Installation Guide to get started.

Feature Detection

Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be undefined.

:::warning Types will not help you here. The types WXT provides for browser assume all APIs exist. You are responsible for knowing whether an API is available or not. :::

To check if an API is available, use feature detection:

if (browser.runtime.onSuspend != null) {
  browser.runtime.onSuspend.addListener(() => {
    // ...
  });
}

Here, optional chaining is your best friend:

browser.runtime.onSuspend?.addListener(() => {
  // ...
});

Alternatively, if you're trying to use similar APIs under different names (to support MV2 and MV3), you can do something like this:

(browser.action ?? browser.browser_action).onClicked.addListener(() => {
  //
});

Augmenting the Browser Type

WXT's browser types are based on the @types/chrome package. That means some Firefox-specific APIs may not be typed, like browser.sidebarAction. If you want to add types for these APIs, you can augment the browser type to add them yourself:

// <srcDir>/browser-types.d.ts
import '@wxt-dev/browser';
import type { SidebarAction } from 'webextension-polyfill';

declare module '@wxt-dev/browser' {
  namespace Browser {
    export const sidebarAction: SidebarAction.Static;
  }
}

For this to work, you may need to install @wxt-dev/browser as a direct dependency.

pnpm add @wxt-dev/browser

Entrypoint Limitations

Because WXT imports your entrypoint files into a NodeJS, non-extension environment, the chrome/browser variables provided to extensions by the browser will not be available.

To prevent some basic errors, WXT polyfills these globals with the same in-memory, fake implementation it uses for testing: @webext-core/fake-browser. However, not all the APIs have been implemented.

So it is extremely important to NEVER use browser.* extension APIs outside the main function of any JS/TS entrypoints (background, content scripts, and unlisted scripts). If you do, you'll see an error like this:

✖ Command failed after 440 ms

 ERROR  Browser.action.onClicked.addListener not implemented.

The fix is simple, just move your API usage into the entrypoint's main function:

:::code-group

browser.action.onClicked.addListener(() => {
  /* ... */
}); 

export default defineBackground(() => {
  browser.action.onClicked.addListener(() => {
    /* ... */
  }); 
});
browser.runtime.onMessage.addListener(() => {
  /* ... */
}); 

export default defineContentScript({
  main() {
    browser.runtime.onMessage.addListener(() => {
      /* ... */
    }); 
  },
});
browser.runtime.onMessage.addListener(() => {
  /* ... */
}); 

export default defineUnlistedScript(() => {
  browser.runtime.onMessage.addListener(() => {
    /* ... */
  }); 
});

:::

Read Entrypoint Loaders for more technical details about this limitation.


url: /guide/essentials/assets.html title: Assets

Are you an LLM? You can read better optimized documentation at /guide/essentials/assets.md for this page in Markdown format

Assets

/assets Directory

Any assets imported or referenced inside the <srcDir>/assets/ directory will be processed by WXT's bundler.

Here's how you access them:

:::code-group

import imageUrl from '~/assets/image.png';

const img = document.createElement('img');
img.src = imageUrl;
<!-- In HTML tags, you must use the relative path --->
<img src="../assets/image.png" />
.bg-image {
  background-image: url(~/assets/image.png);
}
<script>
import image from '~/assets/image.png';
</script>

<template>
  <img :src="image" />
</template>
import image from '~/assets/image.png';

<img src={image} />;

:::

/public Directory

Files inside <rootDir>/public/ are copied into the output folder as-is, without being processed by WXT's bundler.

Here's how you access them:

:::code-group

import imageUrl from '/image.png';

const img = document.createElement('img');
img.src = imageUrl;
<img src="/image.png" />
.bg-image {
  background-image: url(/image.png);
}
<template>
  <img src="/image.png" />
</template>
<img src="/image.png" />

:::

:::warning Assets in the public/ directory are not accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's web_accessible_resources array. :::

Inside Content Scripts

Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname.

But, inside content scripts, the hostname is whatever the tab is set to. So if you try to fetch the asset, manually or as an <img>'s src, it will be loaded from the tab's website, not your extension.

To fix this, you need to convert the image to a full URL using browser.runtime.getURL:

entrypoints/content.ts

import iconUrl from '/icon/128.png';

export default defineContentScript({
  matches: ['*://*.google.com/*'],
  main() {
    console.log(iconUrl); // "/icon/128.png"
    console.log(browser.runtime.getURL(iconUrl)); // "chrome-extension://<id>/icon/128.png"
  },
});

WASM

How a .wasm file is loaded varies greatly between packages, but most follow a basic setup: Use a JS API to load and execute the .wasm file.

For an extension, that means two things:

  1. The .wasm file needs to be present in output folder so it can be loaded.
  2. You must import the JS API to load and initialize the .wasm file, usually provided by the NPM package.

For an example, let's say you have a content script needs to parse TS code into AST. We'll use @oxc-parser/wasm to do it!

First, we need to copy the .wasm file to the output directory. We'll do it with a WXT module:

// modules/oxc-parser-wasm.ts
import { resolve } from 'node:path';

export default defineWxtModule((wxt) => {
  wxt.hook('build:publicAssets', (_, assets) => {
    assets.push({
      absoluteSrc: resolve(
        'node_modules/@oxc-parser/wasm/web/oxc_parser_wasm_bg.wasm',
      ),
      relativeDest: 'oxc_parser_wasm_bg.wasm',
    });
  });
});

Run wxt build, and you should see the WASM file copied into your .output/chrome-mv3 folder!

Next, since this is in a content script and we'll be fetching the WASM file over the network to load it, we need to add the file to the web_accessible_resources:

wxt.config.ts

export default defineConfig({
  manifest: {
    web_accessible_resources: [
      {
        // We'll use this matches in the content script as well
        matches: ['*://*.github.com/*'],
        // Use the same path as `relativeDest` from the WXT module
        resources: ['/oxc_parser_wasm_bg.wasm'],
      },
    ],
  },
});

And finally, we need to load and initialize the .wasm file inside the content script to use it:

entrypoints/content.ts

import initWasm, { parseSync } from '@oxc-parser/wasm';

export default defineContentScript({
  matches: '*://*.github.com/*',
  async main(ctx) {
    if (!location.pathname.endsWith('.ts')) return;

    // Get text from GitHub
    const code = document.getElementById(
      'read-only-cursor-text-area',
    )?.textContent;
    if (!code) return;
    const sourceFilename = document.getElementById('file-name-id')?.textContent;
    if (!sourceFilename) return;

    // Load the WASM file:
    await initWasm({
      module_or_path: browser.runtime.getURL('/oxc_parser_wasm_bg.wasm'),
    });

    // Once loaded, we can use `parseSync`!
    const ast = parseSync(code, { sourceFilename });
    console.log(ast);
  },
});

This code is taken directly from @oxc-parser/wasm docs with one exception: We manually pass in a file path. In a standard NodeJS or web project, the default path works just fine so you don't have to pass anything in. However, extensions are different. You should always explicitly pass in the full URL to the WASM file in your output directory, which is what browser.runtime.getURL returns.

Run your extension, and you should see OXC parse the TS file!


url: /guide/essentials/target-different-browsers.html title: Targeting Different Browsers

Are you an LLM? You can read better optimized documentation at /guide/essentials/target-different-browsers.md for this page in Markdown format

Targeting Different Browsers

When building an extension with WXT, you can create multiple builds of your extension targeting different browsers and manifest versions.

Target a Browser

Use the -b CLI flag to create a separate build of your extension for a specific browser. By default, chrome is targeted.

wxt            # same as: wxt -b chrome
wxt -b firefox
wxt -b custom

During development, if you target Firefox, Firefox will open. All other strings open Chrome by default. To customize which browsers open, see Set Browser Binaries.

Additionally, WXT defines several constants you can use at runtime to detect which browser is in use:

if (import.meta.env.BROWSER === 'firefox') {
  console.log('Do something only in Firefox builds');
}
if (import.meta.env.FIREFOX) {
  // Shorthand, equivalent to the if-statement above
}

Read about Built-in Environment Variables for more details.

Target a Manifest Version

To target specific manifest versions, use the --mv2 or --mv3 CLI flags.

:::tip Default Manifest Version By default, WXT will target MV2 for Safari and Firefox and MV3 for all other browsers. :::

Similar to the browser, you can get the target manifest version at runtime using the built-in environment variable:

if (import.meta.env.MANIFEST_VERSION === 2) {
  console.log('Do something only in MV2 builds');
}

Filtering Entrypoints

Every entrypoint can be included or excluded when targeting specific browsers via the include and exclude options.

Here are some examples:

  • Content script only built when targeting firefox:
export default defineContentScript({  
  include: ['firefox'],  
  main(ctx) {  
    // ...  
  },  
});  
  • HTML file only built for all targets other than chrome:
<!doctype html>  
<html lang="en">  
  <head>  
    <meta charset="UTF-8" />  
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />  
    <meta name="manifest.exclude" content="['chrome', ...]" />  
  </head>  
  <body>  
    <!-- ... -->  
  </body>  
</html>  

Alternatively, you can use the filterEntrypoints config to list all the entrypoints you want to build.


url: /guide/essentials/content-scripts.html title: Content Scripts

Are you an LLM? You can read better optimized documentation at /guide/essentials/content-scripts.md for this page in Markdown format

Content Scripts

To create a content script, see Entrypoint Types.

Context

The first argument to a content script's main function is its "context".

// entrypoints/example.content.ts
export default defineContentScript({
  main(ctx) {},
});

This object is responsible for tracking whether or not the content script's context is "invalidated". Most browsers, by default, do not stop content scripts if the extension is uninstalled, updated, or disabled. When this happens, content scripts start reporting this error:

Error: Extension context invalidated.

The ctx object provides several helpers to stop asynchronous code from running once the context is invalidated:

ctx.addEventListener(...);
ctx.setTimeout(...);
ctx.setInterval(...);
ctx.requestAnimationFrame(...);
// and more

You can also check if the context is invalidated manually:

if (ctx.isValid) {
  // do something
}
// OR
if (ctx.isInvalid) {
  // do something
}

CSS

In regular web extensions, CSS for content scripts is usually a separate CSS file, that is added to a CSS array in the manifest:

{
  "content_scripts": [
    {
      "css": ["content/style.css"],
      "js": ["content/index.js"],
      "matches": ["*://*/*"]
    }
  ]
}

In WXT, to add CSS to a content script, simply import the CSS file into your JS entrypoint, and WXT will automatically add the bundled CSS output to the css array.

// entrypoints/example.content/index.ts
import './style.css';

export default defineContentScript({
  // ...
});

To create a standalone content script that only includes a CSS file:

  1. Create the CSS file: entrypoints/example.content.css
  2. Use the build:manifestGenerated hook to add the content script to the manifest:
    wxt.config.ts
export default defineConfig({  
  hooks: {  
    'build:manifestGenerated': (wxt, manifest) => {  
      manifest.content_scripts ??= [];  
      manifest.content_scripts.push({  
        // Build extension once to see where your CSS get's written to  
        css: ['content-scripts/example.css'],  
        matches: ['*://*/*'],  
      });  
    },  
  },  
});  

UI

WXT provides 3 built-in utilities for adding UIs to a page from a content script:

Each has their own set of advantages and disadvantages.

MethodIsolated StylesIsolated EventsHMRUse page's context
Integrated
Shadow Root✅ (off by default)
IFrame

Integrated

Integrated content script UIs are injected alongside the content of a page. This means that they are affected by CSS on that page.

:::code-group

// entrypoints/example-ui.content.ts
export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Append children to the container
        const app = document.createElement('p');
        app.textContent = '...';
        container.append(app);
      },
    });

    // Call mount to add the UI to the DOM
    ui.mount();
  },
});
// entrypoints/example-ui.content/index.ts
import { createApp } from 'vue';
import App from './App.vue';

export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Create the app and mount it to the UI container
        const app = createApp(App);
        app.mount(container);
        return app;
      },
      onRemove: (app) => {
        // Unmount the app when the UI is removed
        app.unmount();
      },
    });

    // Call mount to add the UI to the DOM
    ui.mount();
  },
});
// entrypoints/example-ui.content/index.tsx
import ReactDOM from 'react-dom/client';
import App from './App.tsx';

export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Create a root on the UI container and render a component
        const root = ReactDOM.createRoot(container);
        root.render(<App />);
        return root;
      },
      onRemove: (root) => {
        // Unmount the root when the UI is removed
        root.unmount();
      },
    });

    // Call mount to add the UI to the DOM
    ui.mount();
  },
});
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
import { mount, unmount } from 'svelte';

export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Create the Svelte app inside the UI container
        return mount(App, { target: container });
      },
      onRemove: (app) => {
        // Destroy the app when the UI is removed
        unmount(app);
      },
    });

    // Call mount to add the UI to the DOM
    ui.mount();
  },
});
// entrypoints/example-ui.content/index.ts
import { render } from 'solid-js/web';

export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Render your app to the UI container
        const unmount = render(() => <div>...</div>, container);
        return unmount;
      },
      onRemove: (unmount) => {
        // Unmount the app when the UI is removed
        unmount();
      },
    });

    // Call mount to add the UI to the DOM
    ui.mount();
  },
});

:::

See the API Reference for the complete list of options.

Shadow Root

Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The ShadowRoot API is ideal for this.

WXT's createShadowRootUi abstracts all the ShadowRoot setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional isolateEvents parameter to further isolate user interactions.

To use createShadowRootUi, follow these steps:

  1. Import your CSS file at the top of your content script
  2. Set cssInjectionMode: "ui" inside defineContentScript
  3. Define your UI with createShadowRootUi()
  4. Mount the UI so it is visible to users

:::code-group

// 1. Import the style
import './style.css';

export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. Set cssInjectionMode
  cssInjectionMode: 'ui',

  async main(ctx) {
    // 3. Define your UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount(container) {
        // Define how your UI will be mounted inside the container
        const app = document.createElement('p');
        app.textContent = 'Hello world!';
        container.append(app);
      },
    });

    // 4. Mount the UI
    ui.mount();
  },
});
// 1. Import the style
import './style.css';
import { createApp } from 'vue';
import App from './App.vue';

export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. Set cssInjectionMode
  cssInjectionMode: 'ui',

  async main(ctx) {
    // 3. Define your UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Define how your UI will be mounted inside the container
        const app = createApp(App);
        app.mount(container);
        return app;
      },
      onRemove: (app) => {
        // Unmount the app when the UI is removed
        app?.unmount();
      },
    });

    // 4. Mount the UI
    ui.mount();
  },
});
// 1. Import the style
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';

export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. Set cssInjectionMode
  cssInjectionMode: 'ui',

  async main(ctx) {
    // 3. Define your UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Container is a body, and React warns when creating a root on the body, so create a wrapper div
        const app = document.createElement('div');
        container.append(app);

        // Create a root on the UI container and render a component
        const root = ReactDOM.createRoot(app);
        root.render(<App />);
        return root;
      },
      onRemove: (root) => {
        // Unmount the root when the UI is removed
        root?.unmount();
      },
    });

    // 4. Mount the UI
    ui.mount();
  },
});
// 1. Import the style
import './style.css';
import App from './App.svelte';
import { mount, unmount } from 'svelte';

export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. Set cssInjectionMode
  cssInjectionMode: 'ui',

  async main(ctx) {
    // 3. Define your UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Create the Svelte app inside the UI container
        return mount(App, { target: container });
      },
      onRemove: (app) => {
        // Destroy the app when the UI is removed
        unmount(app);
      },
    });

    // 4. Mount the UI
    ui.mount();
  },
});
// 1. Import the style
import './style.css';
import { render } from 'solid-js/web';

export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. Set cssInjectionMode
  cssInjectionMode: 'ui',

  async main(ctx) {
    // 3. Define your UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // Render your app to the UI container
        const unmount = render(() => <div>...</div>, container);
      },
      onRemove: (unmount) => {
        // Unmount the app when the UI is removed
        unmount?.();
      },
    });

    // 4. Mount the UI
    ui.mount();
  },
});

:::

See the API Reference for the complete list of options.

Full examples:

:::warning rem Units Are Not Fully Isolated WXT resets most inherited styles via all: initial. This doesn't reset the <html> element's font size, which determines the relative size of rem units.

If your CSS framework uses rem units, like Tailwind CSS, you may notice your UI's scale changing on different websites.

See the FAQ for a fix. :::

IFrame

If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, HMR is supported.

WXT provides a helper function, createIframeUi, which simplifies setting up the IFrame.

  1. Create an HTML page that will be loaded into your IFrame:
<!-- entrypoints/example-iframe.html -->  
<!doctype html>  
<html lang="en">  
  <head>  
    <meta charset="UTF-8" />  
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />  
    <title>Your Content Script IFrame</title>  
  </head>  
  <body>  
    <!-- ... -->  
  </body>  
</html>  
  1. Add the page to the manifest's web_accessible_resources:
    wxt.config.ts
export default defineConfig({  
  manifest: {  
    web_accessible_resources: [  
      {  
        resources: ['example-iframe.html'],  
        matches: [...],  
      },  
    ],  
  },  
});  
  1. Create and mount the IFrame:
export default defineContentScript({  
  matches: ['<all_urls>'],  
  main(ctx) {  
    // Define the UI  
    const ui = createIframeUi(ctx, {  
      page: '/example-iframe.html',  
      position: 'inline',  
      anchor: 'body',  
      onMount: (wrapper, iframe) => {  
        // Add styles to the iframe like width  
        iframe.width = '123';  
      },  
    });  
    // Show UI to user  
    ui.mount();  
  },  
});  

See the API Reference for the complete list of options.

Isolated World vs Main World

By default, all content scripts run in an isolated context where only the DOM is shared with the webpage it is running on - an "isolated world". In MV3, Chromium introduced the ability to run content scripts in the "main" world - where everything, not just the DOM, is available to the content script, just like if the script were loaded by the webpage.

You can enable this for a content script by setting the world option:

export default defineContentScript({
  world: 'MAIN',
});

However, this approach has several notable drawbacks:

  • Doesn't support MV2
  • world: "MAIN" is only supported by Chromium browsers
  • Main world content scripts don't have access to the extension API

Instead, WXT recommends injecting a script into the main world manually using it's injectScript function. This will address the drawbacks mentioned before.

  • injectScript supports both MV2 and MV3
  • injectScript supports all browsers
  • Having a "parent" content script means you can send messages back and forth, making it possible to access the extension API

To use injectScript, we need two entrypoints, one content script and one unlisted script:

📂 entrypoints/
   📄 example.content.ts
   📄 example-main-world.ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
  console.log('Hello from the main world');
});
// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*/*'],
  async main() {
    console.log('Injecting script...');
    await injectScript('/example-main-world.js', {
      keepInDom: true,
    });
    console.log('Done!');
  },
});
export default defineConfig({
  manifest: {
    // ...
    web_accessible_resources: [
      {
        resources: ["example-main-world.js"],
        matches: ["*://*/*"],
      }
    ]
  }
});

injectScript works by creating a script element on the page pointing to your script. This loads the script into the page's context so it runs in the main world.

injectScript returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it.

:::warning Warning: run_at Caveat For MV3, injectScript is synchronous and the injected script will be evaluated at the same time as your the content script's run_at.

However for MV2, injectScript has to fetch the script's text content and create an inline <script> block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's run_at. :::

The script element can be modified just before it is added to the DOM by using the modifyScript option. This can be used to e.g. modify script.async/script.defer, add event listeners to the element, or pass data to the script via script.dataset. An example:

// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*/*'],
  async main() {
    await injectScript('/example-main-world.js', {
      modifyScript(script) {
        script.dataset['greeting'] = 'Hello there';
      },
    });
  },
});
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
  console.log(document.currentScript?.dataset['greeting']);
});

injectScript returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via document.currentScript. An example of bidirectional communication:

// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*/*'],
  async main() {
    const { script } = await injectScript('/example-main-world.js', {
      modifyScript(script) {
        // Add a listener before the injected script is loaded.
        script.addEventListener('from-injected-script', (event) => {
          if (event instanceof CustomEvent) {
            console.log(`${event.type}:`, event.detail);
          }
        });
      },
    });

    // Send an event after the injected script is loaded.
    script.dispatchEvent(
      new CustomEvent('from-content-script', {
        detail: 'General Kenobi',
      }),
    );
  },
});
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
  const script = document.currentScript;

  script?.addEventListener('from-content-script', (event) => {
    if (event instanceof CustomEvent) {
      console.log(`${event.type}:`, event.detail);
    }
  });

  script?.dispatchEvent(
    new CustomEvent('from-injected-script', {
      detail: 'Hello there',
    }),
  );
});

Mounting UI to dynamic element

In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the autoMount API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the anchor option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.

export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      // It observes the anchor
      anchor: '#your-target-dynamic-element',
      onMount: (container) => {
        // Append children to the container
        const app = document.createElement('p');
        app.textContent = '...';
    

… [truncated — open the raw llms.txt above for the full file]
Related

The AI Toolkit for TypeScript, from the creators of Next.js.

/llms.txt
136,985 tokens
Developer Tools

Meet the modern standard for public facing documentation. Beautiful out of the box, easy to maintain, and optimized for user engagement.

/llms.txt
5,436 tokens
/llms-full.txt
181,290 tokens
Developer Tools

Web development for the rest of us.

/llms.txt
602 tokens
/llms-full.txt
453,623 tokens
Developer Tools

Search through billions of items for similar matches to any object, in milliseconds. It’s the next generation of search, an API call away.

/llms.txt
15,715 tokens
/llms-full.txt
588,629 tokens
Developer Tools

Build and deploy reliable background jobs with no timeouts and no infrastructure to manage.

/llms.txt
12,202 tokens
/llms-full.txt
387,586 tokens
Developer Tools

Get the simple developer experience of SQLite in production, and scale your multi-tenant backend with unlimited databases.

/llms.txt
10,006 tokens
/llms-full.txt
163,317 tokens
Developer Tools

Upstash is a serverless data platform providing low latency and high scalability for real-time applications.

/llms.txt
52,307 tokens
/llms-full.txt
1,200,134 tokens
Developer Tools

One-click deployments built for teams, tuned for Laravel, loaded with tools and goodies you're going to love.

/llms.txt
565 tokens
/llms-full.txt
11,330 tokens
Developer Tools