> ## 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 step by step

> 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         |

## 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 a minimal extension named `github-search`.

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

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

  ```bash yarn 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`), pin the version or refresh the cache:

  ```bash theme={null}
  deno run -A --reload npm:extension@latest create github-search
  # or pin an exact version
  deno run -A npm:extension@4.0.1 create github-search
  ```

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

## Step 2: create the manifest file

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.loom.com/embed/1193dc69f7b74a56a5f5d9e0324c255d?sid=99132929-4c05-40e7-b804-3f242daf95ea" title="Create the manifest file" loading="lazy" referrerPolicy="no-referrer-when-downgrade" allow="clipboard-write; encrypted-media; fullscreen" allowFullScreen />
</Frame>

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.

```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 `service_worker.js`:

```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

Your `package.json` file now looks like this:

```json theme={null}
{
  "scripts": {
    "dev": "extension dev",
    "start": "extension start",
    "build": "extension build"
  },
  "devDependencies": {
    "extension": "latest"
  }
}
```

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

<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`.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.loom.com/embed/777544977a32444ba6de4ff23bdaccbc?sid=360eb1b1-af3a-480b-9e71-41a7fb01ca6e" title="Load and run your first extension" loading="lazy" referrerPolicy="no-referrer-when-downgrade" allow="clipboard-write; encrypted-media; fullscreen" allowFullScreen />
</Frame>

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.
