react-chrome-extension: a Vite and Manifest V3 boilerplate that injects React into the page
chrome extension boilerplate with ReactJs using inject page strategy
At a glance
- What is it?
- react-chrome-extension is a boilerplate for Chrome extensions that skips Chrome's popup and injects a React sidebar into the page inside an iframe. It builds with Vite and CRXJS, generates its manifest from TypeScript, and keeps styles isolated from the host page.
- Who is it for?
- react-chrome-extension suits developers who need a sidebar injected into a web page rather than a popup, who want React with real hot module replacement, and who need their extension styles to be unable to touch the sites their users visit. It is the wrong choice for a project that only needs a small popup, and it is the wrong choice if you cannot accept that the injected sidebar only appears on tabs that were navigated after the extension was installed or reloaded.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The boilerplate injects a sidebar because a popup was not good enough
The motivation behind react-chrome-extension is stated as three reasons, and they are worth reading in order because each one constrains the design. First, instead of Chrome's ready-made popup, the author wanted their own page injected into the DOM as a sidebar for better UX. Second, they wanted React for its component model and its build tooling. Third, they needed to guarantee that the extension's CSS could never conflict with the host page's styles. That third requirement is the one that shapes the architecture, and it is what rules out the naive approach of injecting a React root directly into the page. The project description calls it a boilerplate for Chrome extensions using the inject-page strategy, and the README frames it as a way to stop worrying about configuration and start writing components. The project is MIT licensed, the repository publishes no GitHub releases, and the last push was on 2026-10-16. It is sponsored by Recast Studio, and the README links to both the 2018 post that explains why the inject-page strategy is worth using and a newer write-up covering how the current Vite, MV3 and React 19 setup works, noting that the older article's build steps no longer apply.
The manifest is generated from manifest.config.ts, not committed by hand
Manifest V3 support here comes with a specific mechanism. The manifest is generated from manifest.config.ts, so the README says it can never drift from package.json. That is a real class of bug removed: version numbers in the manifest and in the package metadata drift apart, and the extension in the store no longer matches the source. The build is Vite with the CRXJS plugin, and vite.config.ts holds that setup. The TypeScript is strict and fully type-checked, and the typecheck script runs tsc with no emit, so a type error fails the build rather than shipping. The README explains the benefit of the generated manifest in terms of what it removes rather than what it adds, which tells you the author treats it as a reliability measure. The cost is that the manifest is not a file you can open and read in the repository, and any tooling that expects a static manifest.json at a known path will not find it. The README also flags this directly for people arriving from the older webpack version of the boilerplate, where the manifest used to be committed at public/manifest.json.
Two build outputs share the dist folder but only one hot reloads
The development workflow is the part of this boilerplate that has clearly been used hard. Installation is a single npm install, and the development command starts the dev server:
npm installnpm run devThis writes an unpacked extension to dist/ and keeps it in sync as you edit. Component edits hot-reload into the running sidebar, and changes to the background service worker reload the extension automatically. The production build uses the same output directory:
npm run buildbut the README is emphatic that the two are not interchangeable. Only the npm run dev output is wired for hot reload, so if you loaded a production build, you should stop, re-run npm run dev, and hit the reload icon on the extension card. A combined command exists that runs the same sequence as CI, type-checking then linting then building:
npm run checkAnd a packaging step produces an uploadable archive:
npm run zipThe npm script for that last one is worth reading because it shows the shape of the deliverable: it builds, removes any previous extension.zip, changes into dist, zips the contents quietly, and echoes a confirmation. The CI pipeline described in the features list type-checks, lints, builds, and uploads a packaged extension.zip.
The iframe is what makes the style isolation guarantee true
The style isolation is not achieved with careful CSS naming. The sidebar renders inside an iframe, so the host page's stylesheet cannot reach it at all. Getting the extension's own CSS into that iframe is the interesting half, and the mechanism is a Vite import suffix:
import appCss from '../styles/app.css?inline'The ?inline suffix returns the compiled CSS as a string instead of injecting it into the page, and Sidebar.tsx puts that string into a style tag in the iframe's head. Two consequences follow, both stated in the README. Styles meant for the sidebar can never touch the host page, and the extension needs no web_accessible_resources entry for stylesheets. There is exactly one stylesheet that is injected into the visited page, src/content/host.css, and its only job is to position the container. The README insists that every rule in that file must be scoped to #my-extension-root, and points out that a bare body rule there would restyle every site the user visits, noting that this boilerplate used to ship exactly that bug. A related detail explains why App.tsx deliberately does not import its own stylesheet: each surface supplies it, the sidebar inlining it into the iframe while the popup and options pages import it normally.
Content scripts only run on navigation, which surprises new users
There is a behaviour in this boilerplate that costs people an afternoon, and the README calls it out in a note. Content scripts are only injected on navigation, so tabs that were already open when you installed or reloaded the extension need a refresh before the toolbar button will do anything there. That is a property of the extension lifecycle rather than a bug in the boilerplate, but it interacts badly with the way people expect an extension to behave: you install it, you look at a tab you already had open, and nothing happens. The loading instructions are otherwise short. You go to chrome://extensions, switch on Developer mode, click Load unpacked, and select the dist folder, not the project root. Then you open any website and click the extension icon, and the injected sidebar toggles. The bold warning about the project root is there because selecting it produces a Manifest file is missing or unreadable error, since there is no manifest at the root to find now that it is generated at build time.
chrome.storage.sync is wrapped in a typed API with a write budget
The options and storage layer is more considered than most boilerplates provide. src/lib/settings.ts wraps chrome.storage.sync in a typed API, and a useSettings() hook keeps any React tree in sync with it. The options page writes the sidebar's side and width, and the content script subscribes and repositions the container live, in every open tab. The README describes one pattern in Options.tsx that is worth copying even if you never use this boilerplate. The width slider holds its value locally while being dragged and writes once on release. The reason is a hard limit: chrome.storage.sync permits only 120 writes per minute, which a range input firing on every step would exhaust in a single drag. That is the kind of detail that only appears in a boilerplate whose author hit the ceiling in production. The project structure listing shows the related pieces, with lib/ described as typed messaging and settings helpers, and options/ as the options page that reads and writes chrome.storage.
Switching to a popup means giving up the sidebar toggle
An optional popup page is included but deliberately not wired to the toolbar button, and the README explains why in a way that tells you about Chrome's extension API rather than about the project. Because declaring a popup stops chrome.action.onClicked from firing, and that event is what toggles the sidebar, adding a popup silently breaks the sidebar. The fix is a small edit to manifest.config.ts:
action: {
default_icon: icons,
default_popup: 'src/popup/index.html',
},The README is blunt about the consequence: the sidebar toggle stops working at that point, and the background service worker's click listener becomes dead code. That is the shape of the trade in this API, one declaration in the manifest changes which events fire. It also means the popup directory is built and type-checked even when unused, so the build stays honest about what compiles. Lazy mounting is the other thing worth noting from the feature list: React only mounts on pages where the user actually opens the sidebar, which keeps the extension from paying a React startup cost on every page the user visits.
Editorial conclusion
react-chrome-extension suits developers who need a sidebar injected into a web page rather than a popup, who want React with real hot module replacement, and who need their extension styles to be unable to touch the sites their users visit. It is the wrong choice for a project that only needs a small popup, and it is the wrong choice if you cannot accept that the injected sidebar only appears on tabs that were navigated after the extension was installed or reloaded. Verify first that your Node version satisfies the engine range, that every rule in src/content/host.css is scoped to #my-extension-root, and that you read the migration notes if you are coming from the webpack version of this boilerplate, because the output directory moved from build/ to dist/.
Frequently asked questions
How do I load react-chrome-extension into Chrome for development?
Run npm run dev, then go to chrome://extensions, switch on Developer mode, click Load unpacked and select the dist folder rather than the project root. The README warns that selecting the root gives a Manifest file is missing or unreadable error.
Why does the extension sidebar not appear on tabs I already had open?
Content scripts are only injected on navigation, so tabs that were already open when you installed or reloaded the extension need a refresh before the toolbar button does anything there.
How does react-chrome-extension keep its CSS out of the host page?
The sidebar renders inside an iframe, and the CSS is imported with Vite's ?inline suffix and placed in a style tag inside the iframe head. Only src/content/host.css is injected into the visited page, and every rule in it must be scoped to #my-extension-root.
What Node version does react-chrome-extension need?
The package metadata sets the engines field to ^20.19.0 || >=22.12.0, and the README notes the version in .nvmrc is what CI uses.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/satendra02-react-chrome-extension)Was this useful?