Blogsby Darshan S

Search⌘ K

Command Palette

Search for a command to run...

Easily start your chrome extension development using create-crxjs

Easily start your chrome extension development using create-crxjs

Published on 2025-08-246 min read

In order to start developing your chrome extension, you need to have a chrome extension project with a manifest.json file and with components like a popup, a background, and a content script etc. You can do this manually by creating a chrome extension project from scratch by manually creating a manifest.json file, background.js file, content.js file, popup.html file, etc. If you want to use stuff like React, Vite, Tailwind CSS, etc. you have to manually configure them. But with tools like create-crxjs, you can easily scaffold your chrome extension project with Manifest v3 and start developing your chrome extension in minutes.

For a long time the support for Vite in crxjs was in beta. From @crxjs/vite-plugin@2.0.0, Vite is officially supported with hot module replacement (HMR).

Get started with crxjs + Vite + React

  1. Run create-crxjs with your desired package manager.

pnpm dlx create-crxjs

This opens a interactive CLI to scaffold your chrome extension project. Select your desired options from React, Vue, Svelte, Solid and install the dependencies.

create-crxjs CLIcreate-crxjs CLI

  1. Start the extension like any other Vite project using the dev command.

CRX in dev modeCRX in dev mode

  1. To load the extension in Chrome, go to manage extensions in chrome, you need to enable developer mode and then load the unpacked extension. Just select the dist folder from your project folder.

Load unpacked extensionLoad unpacked extension

  1. Click on the extension icon to open the popup. Open the project in the editor and start developing. You can add packages like react-router, tailwindcss, shadcn, lucide-react, etc. like any other Vite project.

Open popupOpen popup

  1. To build the extension, just run the build command. This creates a zip file in the release folder which you can drag and drop in the chrome extensions page in developer mode.

Manifest config file

The project is scaffolded with a manifest.config.ts file which gets converted to a manifest.json file on build.

import { defineManifest } from '@crxjs/vite-plugin';
import pkg from './package.json';
 
export default defineManifest({
    manifest_version: 3,
    name: pkg.name,
    version: pkg.version,
    icons: {
        16: 'public/logo16.png',
        32: 'public/logo32.png',
        48: 'public/logo48.png',
        128: 'public/logo128.png',
    },
    action: {
        default_icon: {
            48: 'public/logo48.png',
        },
        default_popup: 'src/popup/index.html',
    },
    permissions: ['sidePanel', 'contentSettings'],
    content_scripts: [
        {
            js: ['src/content/main.tsx'],
            matches: ['https://*/*'],
        },
    ],
    side_panel: {
        default_path: 'src/sidepanel/index.html',
    },
    background: {
        service_worker: 'src/background/index.ts',
        type: 'module',
    },
});
manifest.config.ts after adding icons and background script

Read more about the manifest config file here. Read more about permissions here.

Making sidepanel as default

import { defineManifest } from '@crxjs/vite-plugin';
import pkg from './package.json';
 
export default defineManifest({
    manifest_version: 3,
    name: pkg.name,
    version: pkg.version,
    icons: {
        16: 'public/logo16.png',
        32: 'public/logo32.png',
        48: 'public/logo48.png',
        128: 'public/logo128.png',
    },
    action: {
        default_icon: {
            48: 'public/logo48.png',
        },
        default_title: 'My Extension',
    },
    permissions: ['sidePanel', 'contentSettings'],
    content_scripts: [
        {
            js: ['src/content/main.tsx'],
            matches: ['https://*/*'],
        },
    ],
    side_panel: {
        default_path: 'src/sidepanel/index.html',
    },
    background: {
        service_worker: 'src/background/index.ts',
        type: 'module',
    },
});
manifest.config.ts after making sidepanel as default

chrome.sidePanel
    .setPanelBehavior({ openPanelOnActionClick: true })
    .catch((error) => console.error(error));
background.ts

This will open sidepanel when you click on the extension icon.

SidepanelSidepanel

Shadow DOM

You might want to add shadow DOM to your content script to avoid conflicts with the page styles.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './views/App.tsx';
import cssText from './index.css?inline';
 
console.log('[CRXJS] Hello world from content script!');
 
const host = document.createElement('div');
host.id = 'crxjs-host';
document.body.appendChild(host);
 
const shadowRoot = host.attachShadow({ mode: 'open' });
 
const style = document.createElement('style');
style.textContent = cssText;
shadowRoot.appendChild(style);
 
const container = document.createElement('div');
container.id = 'crxjs-app';
shadowRoot.appendChild(container);
 
createRoot(container).render(
    <StrictMode>
        <App />
    </StrictMode>
);
main.tsx

*,
*::before,
*::after {
    box-sizing: border-box;
}
index.css

Components

Chrome extensions mainly have popup/sidepanel, background script, content script.

Popup/Sidepanel

This is the UI part of the extension. This could be a popup that appears near the extension icon or a sidepanel that appears on the side of the browser.

Content script
Background script
Communication between components

Open and close sidepanel with keyboard shortcut

document.addEventListener('keydown', (event: KeyboardEvent) => {
    if ((event.ctrlKey || event.metaKey) && event.key === 'm') {
        event.preventDefault();
        chrome.runtime.sendMessage({ action: 'open-sidepanel' });
    }
});
keyboard.ts (content script)

chrome.runtime.onMessage.addListener(async (message, sender) => {
    if (message.action === 'open-sidepanel' && sender.tab?.windowId) {
        try {
            await chrome.sidePanel.open({ windowId: sender.tab.windowId });
            chrome.runtime.sendMessage({
                action: 'opened-sidepanel',
            });
        } catch (error) {
            console.error('Failed to open sidepanel:', error);
        }
    }
});
background.ts (background script)

Add content script to manifest.config.ts

content_scripts: [
    {
        js: ['src/content/keyboard.ts'],
        matches: ['<all_urls>'],
        run_at: 'document_start',
    },
],
manifest.config.ts

import crxLogo from '@/assets/crx.svg'
import reactLogo from '@/assets/react.svg'
import viteLogo from '@/assets/vite.svg'
import HelloWorld from '@/components/HelloWorld'
import './App.css'
import { useEffect } from 'react'
 
export default function App() {
  useEffect(() => {
    const messageListener = (message: any) => {
      if (message.action === 'opened-sidepanel') {
        window.close()
      }
    }
 
    chrome.runtime.onMessage.addListener(messageListener);
 
    return () => {
      chrome.runtime.onMessage.removeListener(messageListener);
    }
  }, []);
  return (
    <div>
      <a href="https://vite.dev" target="_blank" rel="noreferrer">
        <img src={viteLogo} className="logo" alt="Vite logo" />
      </a>
      <a href="https://reactjs.org/" target="_blank" rel="noreferrer">
        <img src={reactLogo} className="logo react" alt="React logo" />
      </a>
      <a href="https://crxjs.dev/vite-plugin" target="_blank" rel="noreferrer">
        <img src={crxLogo} className="logo crx" alt="crx logo" />
      </a>
      <HelloWorld msg="Vite + React + CRXJS" />
    </div>
  )
}
 
App.tsx (sidepanel)

Publishing chrome extension

Read more about review process for chrome extension here.

Read more about mistakes while publishing chrome extension to avoid rejections and long review process here.


👉 crxjs docs

👉 Checkout my chrome extension TimestampsGuy that allows you to add timestamps to YouTube videos