> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Create your first browser extension

> Build your first browser extension end-to-end with Extension.js. Create a GitHub search Omnibox shortcut and learn the core development loop.

You will build an Omnibox (address bar) shortcut. Type `gh` in the address bar, enter a query, and land on GitHub search results. Along the way you will wire a `manifest.json` and handle input in a background service worker. You will also practice the dev loop (`create` → `dev` → `build`) that every project follows.

## What you will build

| Capability | What you get |
| - | - |
| Omnibox keyword flow | Trigger extension behavior with `gh` from the browser URL bar |
| Background event handling | Handle user input through a service worker |
| Local dev loop | Run, load, and validate extension behavior quickly |
| Progressive enhancement | Add live GitHub suggestions after baseline flow works |

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/4Em5t3HEEhw?rel=0" title="Extension.js: your first extension" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

## The plan

Make GitHub search as fast as a native browser shortcut. The extension reserves the keyword `gh`; after you type `gh` and a query, it opens GitHub search results.

## Step 1: create the extension

Use the Extension.js `create` command to scaffold an extension named `github-search`.

The default template is a working sidebar starter, so the scaffold already contains
`src/manifest.json` and `src/background.ts`. The next two steps replace those two files.
Everything you write in this tutorial goes in `src/`, because Extension.js prefers
`src/manifest.json` when it is present.

<Note>
  **Using Yarn?** The `yarn dlx` command requires Yarn 2 or later. Yarn 1 does
  not include `dlx` and fails with a "Command not found" error. On Yarn 1, use
  the `npm` tab (`npx`) instead.
</Note>

<CodeGroup>
  ```bash npm theme={null}
  npx extension@latest create github-search
  ```

  ```bash pnpm theme={null}
  pnpx extension@latest create github-search
  ```

  ```bash yarn 2+ theme={null}
  yarn dlx extension@latest create github-search
  ```

  ```bash bun theme={null}
  bunx extension@latest create github-search
  ```

  ```bash deno theme={null}
  deno run -A npm:extension@latest create github-search
  ```
</CodeGroup>

<Note>
  **Using Deno?** Deno caches npm packages aggressively, including the `@latest`
  tag, so `npm:extension@latest` can keep resolving to an older cached release.
  If a scaffold or `dev` build fails with an error that a newer version already
  fixed (for example `manifest.json references files that were not emitted to
      disk`), refresh the cache, or pin an exact release in place of `@latest`:

  ```bash theme={null}
  deno run -A --reload npm:extension@latest create github-search
  ```

  After `deno install`, confirm the resolved version (`extension` should match
  the latest release) before running `deno task dev`.
</Note>

## What create generates

`create` scaffolds a complete project and initializes a git repository. The default template produces this tree:

```text theme={null}
github-search/
├── .extension-create.json
├── .gitignore
├── README.md
├── STORE.md
├── extension-env.d.ts
├── extension.config.js
├── package.json
├── tsconfig.json
└── src/
    ├── manifest.json
    ├── background.ts
    ├── content/
    │   ├── ContentApp.ts
    │   ├── scripts.ts
    │   ├── styles.css
    │   └── types.d.ts
    ├── images/
    │   ├── icon-16.png
    │   ├── icon-32.png
    │   ├── icon-48.png
    │   ├── icon-64.png
    │   ├── icon-128.png
    │   └── icon.png
    └── sidebar/
        ├── SidebarApp.ts
        ├── index.html
        ├── scripts.ts
        └── styles.css
```

The default template is `typescript`, so the tree carries `tsconfig.json` and `extension-env.d.ts` for typed extension APIs. Pass `--template=javascript` for the same starter in plain `.js` files, without those two files.

Two files anchor the layout:

* `package.json` marks the project root. [Special folders](/docs/features/special-folders) (`pages/`, `scripts/`, `public/`) and the `dist/` output resolve from that directory, never from `src/`.
* `manifest.json` marks the extension source. It can sit at the root or in `src/`. When both exist, `src/manifest.json` wins.

What to edit and what to delete when you adapt the scaffold:

| File | What it is | Edit or delete? |
| - | - | - |
| `package.json` | Project root marker, scripts, and the pinned `extension` version. | Keep. Edit the name and description. |
| `extension.config.js` | Per-browser run options such as profiles and browser flags. | Optional. Delete it to restore the defaults. |
| `.extension-create.json` | Records the create version, template, and template ref. | Keep for provenance. Builds do not read it. |
| `README.md`, `STORE.md` | Starter docs and a store listing draft. | Replace or delete. |
| `src/manifest.json` | The extension manifest. | Edit in place. Do not add a second manifest. |
| `src/background.ts` | Background script for the sidebar starter. | Edit or replace, as this tutorial does. |
| `src/content/` | Content script that the starter injects on every page. | Delete it and remove its `content_scripts` entry. |
| `src/sidebar/` | Sidebar panel page for the starter. | Delete it and remove its `side_panel` entries. |
| `src/images/` | Icons that the manifest references. | Keep. Swap in your own icons. |

## Step 2: create the manifest file

Every extension starts with a manifest file. It defines metadata, permissions, and runtime files. Based on the [plan above](#the-plan), set the `gh` shortcut and add a service worker for user events.

Open the `src/manifest.json` the scaffold created and replace its contents with:

```json theme={null}
{
  "manifest_version": 3,
  "name": "GitHub Search",
  "version": "1.0",
  "omnibox": { "keyword": "gh" },
  "background": {
    "service_worker": "service_worker.js"
  }
}
```

* `omnibox.keyword`: When you type `gh`, the browser fires an event.
* `background.service_worker`: Listens to the event you triggered.

## Step 3: create the background service worker

In browser extensions, the background service worker (a script that runs independently of any visible page) handles browser events.

For this example, add a script that listens to Omnibox input and routes the query to GitHub search.

Create `src/service_worker.js`, and delete the `src/background.ts` the scaffold shipped so nothing else claims the background entry:

```js theme={null}
// When the user has accepted what is typed into the omnibox.
chrome.omnibox.onInputEntered.addListener((text) => {
  // Convert any special character (spaces, &, ?, etc)
  // into a valid character for the URL format.
  const encodedSearchText = encodeURIComponent(text);
  const url = `https://github.com/search?q=${encodedSearchText}&type=issues`;

  chrome.tabs.create({ url });
});
```

The script above opens a new tab with GitHub search results whenever you type something after "gh" in the address bar.

## Step 4: load your extension

Move into the project and install its dependencies. `create` scaffolds the project but
does not install for you, and the `dev` script runs the local `extension` binary:

```bash theme={null}
cd github-search
npm install
```

Your `package.json` file now looks like this, with `extension` pinned to the release you scaffolded with:

```json theme={null}
{
  "private": true,
  "name": "github-search",
  "description": "Adds a sidebar panel to the browser with a simple page.",
  "version": "1.0.0",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "extension dev",
    "start": "extension start",
    "build": "extension build",
    "preview": "extension preview",
    "build:chrome": "extension build --browser chrome",
    "build:firefox": "extension build --browser firefox",
    "build:edge": "extension build --browser edge"
  },
  "dependencies": {},
  "devDependencies": {
    "extension": "^4.1.16"
  }
}
```

These scripts are the default Extension.js commands. Run the extension for the first time:

<Note>
  **First run.** `extension dev` targets `chromium` by default and runs your
  extension in a version-pinned Chrome for Testing with an isolated profile, so
  it never touches the browser you use every day. When that browser is not on
  the machine yet, the first run asks to download it once and continues into
  the dev session as soon as it lands.

  Answer `n` and nothing is downloaded. Run `npx extension install chromium`
  whenever you want it, reach a browser you already have with `--browser=edge`
  or `--browser=brave`, pin any binary with `--chromium-binary <path>`, or
  start the dev server alone with `--no-browser`. A non-interactive shell, CI
  included, is never asked: it prints the install command and stops. See
  [why Extension.js downloads a browser](/docs/commands/install#why-extensionjs-downloads-a-browser)
  and [skip the managed download](/docs/commands/install#skip-the-managed-download).
</Note>

<CodeGroup>
  ```bash npm theme={null}
  npm run dev
  ```

  ```bash pnpm theme={null}
  pnpm run dev
  ```

  ```bash yarn theme={null}
  yarn run dev
  ```

  ```bash bun theme={null}
  bun run dev
  ```

  ```bash deno theme={null}
  deno task dev
  ```
</CodeGroup>

If your setup is correct, Extension.js launches Chrome with a fresh profile, loads `github-search` as an unpacked extension, and prints a ready banner in your terminal. The Chrome address bar now recognizes `gh` as a keyword.

Type `gh` followed by a space, enter `extension.js`, press Enter. A new tab opens to `https://github.com/search?q=extension.js&type=issues`.

You now have a working browser extension that searches on GitHub.

## Step 5: make it better

Improve the search experience by adding suggestions directly in the address bar with an Omnibox input listener.

Update `service_worker.js` to fetch GitHub suggestions and display them while typing.

```js title="service_worker.js" theme={null}
// Create a debounce function to avoid excessive
// calls to the GitHub API while the user is still
// typing the search query.
function debounce(fn, delay) {
  let timeoutID;
  return function (...args) {
    if (timeoutID) clearTimeout(timeoutID);
    timeoutID = setTimeout(() => fn(...args), delay);
  };
}

// When the user has changed what is typed into the omnibox.
chrome.omnibox.onInputChanged.addListener(
  debounce(async (text, suggest) => {
    const response = await fetch(
      `https://api.github.com/search/issues?q=${text}`,
    );
    const data = await response.json();
    const suggestions = data.items.map((issue) => ({
      content: issue.html_url,
      description: issue.title,
    }));

    suggest(suggestions);
  }, 250),
);

// When the user has accepted what is typed into the omnibox.
chrome.omnibox.onInputEntered.addListener((text) => {
  // Convert any special character (spaces, &, ?, etc)
  // into a valid character for the URL format.
  const encodedSearchText = encodeURIComponent(text);
  const url = `https://github.com/search?q=${encodedSearchText}&type=issues`;

  chrome.tabs.create({ url });
});
```

This code adds live GitHub suggestions directly in the address bar.

You now have a working GitHub search extension. Iterate on it and adapt it to your own workflow.

## Next steps

* Create another extension with [templates](/docs/getting-started/templates).
* Add automated checks with [Playwright E2E](/docs/workflows/playwright-e2e).
* Review [Troubleshooting](/docs/workflows/troubleshooting), [Security checklist](/docs/workflows/security-checklist), and [Performance playbook](/docs/workflows/performance-playbook) as your extension grows.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.