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

You add new page types by scaffolding a Vite-powered page under pages/, configuring vite.config.mts and tailwind.config.ts, and registering the output in 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:

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:

// 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 and import the shared preset via withUI:

// 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. Follow the pattern used by SidePanel.tsx to leverage shared storage, i18n, and UI utilities:

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 to mount the component:

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 (copy the structure from pages/side-panel/public/index.html).

Step 6: Register in the Extension Manifest

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

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

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 to make it available via @extension/ui:

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

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 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 from an existing page, and adjusting the outDir to dist/<page-name>.
  • Register pages in 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 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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →