# How to Create a Popup and Implement Background Script Communication in a Chrome Extension

> Learn how to create a Chrome extension popup and implement communication with background scripts using React and Vite. Follow our step-by-step guide for seamless integration.

- Repository: [JongHak Seo/chrome-extension-boilerplate-react-vite](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite)
- Tags: how-to-guide
- Published: 2026-03-05

---

**You create a popup by declaring `action.default_popup` in [`manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.ts) and building a React component in [`pages/popup/src/Popup.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/popup/src/Popup.tsx), then communicate with the background Service Worker using `chrome.runtime.sendMessage` and `onMessage` listeners in [`chrome-extension/src/background/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/src/background/index.ts).**

The `chrome-extension-boilerplate-react-vite` repository provides a modern architecture for building Chrome extensions with React and Vite. This guide covers how to configure the popup entry point, build the UI with shared state management, and implement type-safe message passing between the popup and background script.

## Configuring the Popup Entry Point

The boilerplate stores the popup page under `pages/popup`, with the main React component at [`pages/popup/src/Popup.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/popup/src/Popup.tsx). Vite compiles this into [`dist/popup/index.html`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/dist/popup/index.html) during the build process.

To make Chrome recognize this as your extension's popup, declare it in the manifest:

```ts
// chrome-extension/manifest.ts
action: {
  default_popup: 'popup/index.html',   // Entry point for toolbar click
  default_icon: 'icon-34.png',
},

```

This configuration tells Chrome to open [`popup/index.html`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/popup/index.html) whenever a user clicks the extension icon in the toolbar. The file path is relative to the `dist` folder, which mirrors your source structure after the Vite build.

## Building the Popup UI with React

The [`Popup.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/Popup.tsx) component functions like any standard React application, with full access to the `chrome.runtime` and `chrome.tabs` APIs. It typically imports shared storage from `@extension/storage` to maintain state synchronization across extension pages.

Key implementation details from the source:

- Use `useStorage(exampleThemeStorage)` to read and write theme state that persists across popup sessions
- Call `chrome.tabs.create()` to open external links in new browser tabs
- Execute `chrome.scripting.executeScript()` to inject content scripts into the active tab

```tsx
// pages/popup/src/Popup.tsx
const Popup = () => {
  const { isLight } = useStorage(exampleThemeStorage);
  
  const injectContentScript = async () => {
    const [tab] = await chrome.tabs.query({ currentWindow: true, active: true });
    await chrome.scripting.executeScript({
      target: { tabId: tab.id! },
      files: ['/content-runtime/example.iife.js'],
    });
  };

  return (
    <div className={cn('App', isLight ? 'bg-slate-50' : 'bg-gray-800')}>
      <button onClick={injectContentScript}>
        Inject Script
      </button>
      <ToggleButton>{t('toggleTheme')}</ToggleButton>
    </div>
  );
};

export default withErrorBoundary(withSuspense(Popup, <LoadingSpinner />), ErrorDisplay);

```

The component uses `chrome.runtime.getURL()` to resolve static assets like logos and SVGs, ensuring they load correctly from the bundled extension package.

## Implementing Popup-to-Background Communication

While the popup can access many Chrome APIs directly, privileged operations (such as accessing `chrome.storage.session` or batch processing) should be delegated to the background Service Worker. The boilerplate sets up the background script at [`chrome-extension/src/background/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/src/background/index.ts).

### Setting Up the Background Listener

Register message handlers in the background script to process requests from the popup:

```ts
// chrome-extension/src/background/index.ts
import 'webextension-polyfill';
import { exampleThemeStorage } from '@extension/storage';

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
  if (msg.type === 'GET_THEME') {
    exampleThemeStorage.get().then(state => sendResponse({ theme: state.theme }));
    return true; // Required for async sendResponse
  }

  if (msg.type === 'TOGGLE_THEME') {
    exampleThemeStorage.toggle().then(() => sendResponse({ ok: true }));
    return true;
  }

  return false;
});

```

**Critical:** Return `true` from the listener when calling `sendResponse` asynchronously. This keeps the message channel open until the Promise resolves.

### Sending Messages from the Popup

Use `chrome.runtime.sendMessage()` in the popup to dispatch requests to the background:

```tsx
// Inside Popup.tsx or a helper hook
const requestTheme = async () => {
  const response = await chrome.runtime.sendMessage({ type: 'GET_THEME' });
  console.log('Current theme:', response.theme);
};

const toggleThemeViaBackground = async () => {
  await chrome.runtime.sendMessage({ type: 'TOGGLE_THEME' });
};

```

This pattern decouples your UI from implementation details. The background can handle complex logic, storage migrations, or API calls while the popup remains lightweight and focused on presentation.

## Using Long-Lived Ports for Continuous Updates

For real-time data streams (such as live theme updates or progress notifications), establish a persistent connection using `chrome.runtime.connect()` instead of one-time messages.

### Background Port Management

```ts
// Add to chrome-extension/src/background/index.ts
const ports = new Set<chrome.runtime.Port>();

chrome.runtime.onConnect.addListener(port => {
  ports.add(port);
  port.onDisconnect.addListener(() => ports.delete(port));
});

// Broadcast updates to all connected popups
const broadcastThemeChange = (theme: string) => {
  ports.forEach(port => port.postMessage({ type: 'THEME_CHANGED', theme }));
};

```

### Popup Connection Handling

```tsx
// Inside Popup.tsx
useEffect(() => {
  const port = chrome.runtime.connect();
  
  port.onMessage.addListener(msg => {
    if (msg.type === 'THEME_CHANGED') {
      setTheme(msg.theme);
    }
  });
  
  return () => port.disconnect();
}, []);

```

Long-lived ports remain open as long as the popup is active, allowing the background to push updates without polling. For simple request-response patterns, stick with `sendMessage`.

## Step-by-Step Implementation

Follow these steps to add popup-to-background communication to your extension:

1. **Define message types** in a shared package for TypeScript safety:

   ```ts
   // packages/shared/lib/types/message.ts
   export type PopupToBackgroundMsg =
     | { type: 'GET_THEME' }
     | { type: 'TOGGLE_THEME' };
   ```

2. **Import types in the popup** when sending messages:

   ```tsx
   import type { PopupToBackgroundMsg } from '@extension/shared/lib/types/message';
   
   await chrome.runtime.sendMessage<PopupToBackgroundMsg>({ type: 'GET_THEME' });
   ```

3. **Restart the dev server** (`pnpm dev`) and test the flow by inspecting the background Service Worker console at `chrome://extensions`.

4. **Handle errors** by wrapping `sendMessage` calls in try-catch blocks, as disconnected ports or unloaded background pages can throw exceptions.

## Summary

- The popup entry point is declared via `action.default_popup` in [`manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/manifest.ts) and built from [`pages/popup/src/Popup.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/popup/src/Popup.tsx)
- **Background scripts** receive messages through `chrome.runtime.onMessage.addListener()` in [`chrome-extension/src/background/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/src/background/index.ts)
- Always **return `true`** from background listeners when using asynchronous `sendResponse` to prevent message port closure
- Use **one-time messages** (`sendMessage`) for discrete requests and **long-lived ports** (`connect`) for continuous data streams
- The shared `storage` package provides type-safe state management, but message passing offers immediate cross-context updates without storage events

## Frequently Asked Questions

### Can the popup communicate directly with content scripts?

No, the popup cannot directly message content scripts. According to the Chrome extension architecture, you must route communication through the background script: the popup sends a message to the background (`chrome.runtime.sendMessage`), and the background forwards it to the content script (`chrome.tabs.sendMessage`). This triangulation ensures proper context isolation and security boundaries.

### Why does the background listener need to return true?

The listener must return `true` when you plan to send a response asynchronously (such as after awaiting storage operations or API calls). Returning `true` keeps the internal message channel open until `sendResponse` is invoked. If you return `undefined` or `false`, Chrome immediately closes the port, causing "The message port closed before a response was received" errors in the popup.

### How do I handle type safety across popup and background messages?

Define a TypeScript union type for all valid message shapes in the shared package ([`packages/shared/lib/types/message.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/packages/shared/lib/types/message.ts)), then use generic type parameters when calling `sendMessage<T>()`. While this won't enforce runtime safety, it provides compile-time checking that both sender and receiver agree on the message schema, preventing typo-related bugs during development.

### What's the difference between chrome.runtime.sendMessage and chrome.runtime.connect?

**`sendMessage`** establishes a short-lived connection for one-off requests and responses, automatically closing after the background calls `sendResponse`. **`connect`** creates a long-lived port that remains open until explicitly disconnected (`port.disconnect()`), allowing multiple bidirectional messages. Use `sendMessage` for buttons and toggles; use `connect` for live streaming data or keeping UI state synchronized with background processes.