# How to Add New Page Types or Components to the Extension Structure

> Learn how to add new page types and components to your Chrome extension boilerplate with React and Vite. Follow our step-by-step guide for seamless integration and configuration.

- 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 add new page types by scaffolding a Vite-powered page under `pages/`, configuring `vite.config.mts` and [`tailwind.config.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/tailwind.config.ts), and registering the output in [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts); for reusable components, you create them in `packages/ui/lib/components/` and export them via `@extension/ui`.**

The `chrome-extension-boilerplate-react-vite` repository organizes Chrome Extensions as a **monorepo** of independent Vite-powered pages and shared packages. To extend the UI structure, you either create a new page type addressed from the manifest or build a reusable component in the shared UI package. Both approaches follow established patterns found in the existing side-panel, popup, and options pages.

## Adding a New Page Type to the Extension Structure

### Step 1: Scaffold the Page Directory

Create a dedicated folder under `pages/` for your new page type. For example, to add a custom panel named `my-panel`, create the following structure:

```bash
mkdir -p pages/my-panel/src pages/my-panel/public

```

### Step 2: Configure Vite for the New Page

Copy the Vite configuration from an existing page (such as `pages/side-panel/vite.config.mts`) and adjust the `outDir` to match your new page name:

```ts
// pages/my-panel/vite.config.mts
import { resolve } from 'node:path';
import { withPageConfig } from '@extension/vite-config';

const rootDir = resolve(import.meta.dirname);
const srcDir = resolve(rootDir, 'src');

export default withPageConfig({
  resolve: {
    alias: {
      '@src': srcDir,
    },
  },
  publicDir: resolve(rootDir, 'public'),
  build: {
    outDir: resolve(rootDir, '..', '..', 'dist', 'my-panel'),
  },
});

```

### Step 3: Set Up Tailwind CSS (Optional)

If your page uses Tailwind, copy [`pages/side-panel/tailwind.config.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/side-panel/tailwind.config.ts) and import the shared preset via `withUI`:

```ts
// pages/my-panel/tailwind.config.ts
import { withUI } from '@extension/ui';

export default withUI({});

```

### Step 4: Write the Page Component

Create the React component in [`pages/my-panel/src/MyPanel.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/my-panel/src/MyPanel.tsx). Follow the pattern used by [`SidePanel.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/SidePanel.tsx) to leverage shared storage, i18n, and UI utilities:

```tsx
import '@src/MyPanel.css';
import { t } from '@extension/i18n';
import { useStorage, withErrorBoundary, withSuspense } from '@extension/shared';
import { exampleThemeStorage } from '@extension/storage';
import { cn, ErrorDisplay, LoadingSpinner, ToggleButton } from '@extension/ui';

const MyPanel = () => {
  const { isLight } = useStorage(exampleThemeStorage);
  const logo = isLight ? 'my-panel/logo.svg' : 'my-panel/logo_dark.svg';

  const openGitHub = () => chrome.tabs.create({ url: 'https://github.com/jonghakseo' });

  return (
    <div className={cn('App', isLight ? 'bg-slate-50' : 'bg-gray-800')}>
      <header className={cn('App-header', isLight ? 'text-gray-900' : 'text-gray-100')}>
        <button onClick={openGitHub}>
          <img src={chrome.runtime.getURL(logo)} className="App-logo" alt="logo" />
        </button>
        <p>{t('hello', 'World')}</p>
        <ToggleButton onClick={exampleThemeStorage.toggle}>{t('toggleTheme')}</ToggleButton>
      </header>
    </div>
  );
};

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

```

### Step 5: Create the Entry Point and HTML Template

Add [`pages/my-panel/src/index.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/my-panel/src/index.tsx) to mount the component:

```tsx
import { createRoot } from 'react-dom/client';
import MyPanel from './MyPanel';

const container = document.getElementById('root');
if (container) {
  createRoot(container).render(<MyPanel />);
}

```

Provide an HTML template at [`pages/my-panel/public/index.html`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/my-panel/public/index.html) (copy the structure from [`pages/side-panel/public/index.html`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/pages/side-panel/public/index.html)).

### Step 6: Register in the Extension Manifest

Edit [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) to expose the new page. Use an existing key like `side_panel`, `options_page`, or `devtools_page`, or define a custom entry:

```ts
// chrome-extension/manifest.ts
export default defineManifest({
  // ...
  side_panel: {
    default_path: 'side-panel/index.html',
  },
  my_panel: {
    default_path: 'my-panel/index.html',   // ← new entry
  },
  // ...
});

```

Run `pnpm run build` to compile the new page into `dist/my-panel`.

## Adding Reusable Components to the Extension Structure

### Step 1: Create the Component File

Place new shared components in `packages/ui/lib/components/`. For example, create [`packages/ui/lib/components/MyWidget.tsx`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/packages/ui/lib/components/MyWidget.tsx):

```tsx
import { cn } from '@extension/ui';

export interface MyWidgetProps {
  label: string;
  onClick?: () => void;
  className?: string;
}

/**
 * A simple button‑styled widget that respects the current theme
 * (light/dark) via the shared `cn` helper.
 */
export const MyWidget = ({ label, onClick, className }: MyWidgetProps) => (
  <button
    className={cn(
      'px-3 py-1 rounded shadow',
      'bg-blue-500 text-white hover:bg-blue-600',
      className,
    )}
    onClick={onClick}
  >
    {label}
  </button>
);

```

### Step 2: Export from the UI Package Index

Register the component in [`packages/ui/lib/components/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/packages/ui/lib/components/index.ts) to make it available via `@extension/ui`:

```tsx
// packages/ui/lib/components/index.ts
export * from './ToggleButton';
export * from './LoadingSpinner';
export * from './MyWidget';   // ← newly added line

```

### Step 3: Import and Use in Any Page

Now any page type can import and render the component:

```tsx
import { MyWidget } from '@extension/ui';

<MyWidget label={t('myLabel')} onClick={handleClick} />

```

## Understanding the Monorepo Architecture

The boilerplate uses a **manifest-driven routing** system where each page is an independent Vite application. The [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) file declares which HTML files become extension entry points, while shared logic lives in the `@extension/*` packages resolved via TypeScript path aliases.

Key architectural patterns include:

- **Independent Vite builds** – Each page under `pages/<name>/` owns its `vite.config.mts` that merges with the global configuration via `withPageConfig`.
- **Shared UI system** – The `@extension/ui` package provides Tailwind-enabled components and the `cn` class-name helper used across all page types.
- **Storage hooks** – The `useStorage` hook from `@extension/shared` synchronizes state between content scripts and UI pages through the `exampleThemeStorage` implementation.

## Summary

- **Create new pages** by scaffolding a folder under `pages/`, copying `vite.config.mts` and [`tailwind.config.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/tailwind.config.ts) from an existing page, and adjusting the `outDir` to `dist/<page-name>`.
- **Register pages** in [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) using keys like `side_panel`, `options_page`, or custom entries that point to the built HTML.
- **Build reusable components** inside `packages/ui/lib/components/` and export them from [`packages/ui/lib/components/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/packages/ui/lib/components/index.ts) to make them available via `@extension/ui`.
- **Leverage shared utilities** – Use `useStorage`, `withErrorBoundary`, `withSuspense`, and `cn` from the `@extension/*` packages to maintain consistency across the extension structure.

## Frequently Asked Questions

### Where do I register a new page in the manifest?

Register new pages in [`chrome-extension/manifest.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/chrome-extension/manifest.ts) by adding an entry that points to the built HTML file. For side panels use the `side_panel` key, for options pages use `options_page`, and for devtools use `devtools_page`. You can also define custom keys for internal pages, setting `default_path` to `<page-name>/index.html` relative to the `dist` folder.

### Can I use the same component across different page types?

Yes. Place reusable components in `packages/ui/lib/components/` and export them from [`packages/ui/lib/components/index.ts`](https://github.com/jonghakseo/chrome-extension-boilerplate-react-vite/blob/main/packages/ui/lib/components/index.ts). Any page—whether popup, side-panel, or content-ui—can import the component via `import { MyComponent } from '@extension/ui'`. The shared Tailwind configuration ensures consistent styling across all page types.

### How do I handle storage and state in a new page?

Use the `useStorage` hook from `@extension/shared` to synchronize state with Chrome's storage API. Import a concrete storage implementation such as `exampleThemeStorage` from `@extension/storage`, then call `const { isLight } = useStorage(exampleThemeStorage)` inside your component. This pattern ensures reactive updates across all extension contexts including content scripts and background service workers.

### Do I need a separate Vite config for every new page?

Yes. Each page type requires its own `vite.config.mts` under `pages/<page-name>/` to define the build entry point, path aliases, and output directory. Copy the configuration from an existing page like `pages/side-panel/vite.config.mts` and modify the `outDir` to `dist/<page-name>`. The `withPageConfig` helper merges your page-specific settings with the global Vite configuration shared across the monorepo.