
Easily start your chrome extension development using create-crxjs
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
- 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 CLI
- Start the extension like any other Vite project using the dev command.
CRX in dev mode
- 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 extension
- 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 popup
- 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',
},
});
Read more about the manifest config file here. Read more about permissions here.
Making sidepanel as default
- To make the sidepanel as default instead of popup, remove the action.default_popup from the manifest.config.ts file and add the action.default_title. Keep the sidPanel permission, side_panel.default_path.
- Add the following snippet to background script
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',
},
});
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
.catch((error) => console.error(error));
This will open sidepanel when you click on the extension icon.
Sidepanel
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>
);
*,
*::before,
*::after {
box-sizing: border-box;
}
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
- A content script is a JavaScript file that runs in the context of a web page. Unlike the popup, which is its own self-contained environment, a content script can interact directly with the DOM of the page the user is currently viewing.
- Content scripts have access to web page dom and few chrome extension APIs like
chrome.runtime
,chrome.storage
, etc. But they do not have access to all the chrome extension APIs likechrome.tabs
, etc. For that you need to use message passing to communicate with the background script.
Background script
- The background service worker is a central command center for your extension. It's a script that listens for events and handles tasks that need to run independently of the user interface or specific webpages.
- It has access to all the chrome extension APIs and can communicate with the popup and content scripts using message passing.
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' });
}
});
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);
}
}
});
Add content script to manifest.config.ts
content_scripts: [
{
js: ['src/content/keyboard.ts'],
matches: ['<all_urls>'],
run_at: 'document_start',
},
],
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>
)
}
Publishing chrome extension
- To publish your chrome extension to the chrome web store, you need to sign up for a developer account. You need to pay a one time fee of $5 to get a developer account. After that you can upload your zip file from release folder, fill the details and submit it for review.
- The review process takes 1-2 days, then you can see your extension in the chrome web store.
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.
👉 Checkout my chrome extension TimestampsGuy that allows you to add timestamps to YouTube videos