I recently rewrote one of my projects — Minimal Theme for Twitter — as a Next.js Chrome extension because I wanted to use React for the pop-up. Using React would allow me to clearly separate my extension’s pop-up component and its application logic from its content scripts, which are the CSS and JavaScript files needed to execute the functionality of the extension.
As you may know, there are several ways to get started with React, from simply adding script tags to using a recommended toolchain like Create React App, Gatsby, or Next.js. There are some immediate benefits you get from Next.js as a React framework, like the static HTML feature you get with next export
. While features like preloading JavaScript and built-in routing are great, my main goal with rewriting my Chrome extension was better code organization, and that’s really where Next.js shines. It gives you the most out-of-the-box for the least amount of unnecessary files and configuration. I tried fiddling around with Create React App and it has a surprising amount of boilerplate code that I didn’t need.
I thought it might be straightforward to convert over to a Next.js Chrome extension since it’s possible to export a Next.js application to static HTML. However, there are some gotchas involved, and this article is where I tell you about them so you can avoid some mistakes I made.
First, here’s the GitHub repo if you want to skip straight to the code.
New to developing Chrome extensions? Sarah Drasner has a primer to get you started.
Folder structure
next-export
is a post-processing step that compiles your Next.js code, so we don’t need to include the actual Next.js or React code in the extension. This allows us to keep our extension at its lowest possible file size, which is what we want for when the extension is eventually published to the Chrome Web Store.
So, here’s how the code for my Next.js Chrome extension is organized. There are two directories — one for the extension’s code, and one containing the Next.js app.
📂 extension
📄 manifest.json
📂 next-app
📂 pages
📂 public
📂 styles
📄 package.json
README.md
The build script
To use next export
in a normal web project, you would modify the default Next.js build script in package.json
to this:
"scripts": {
"build": "next build && next export"
}
Then, running npm run build
(or yarn build
) generates an out
directory.
In this case involving a Chrome extension, however, we need to export the output to our extension
directory instead of out
. Plus, we have to rename any files that begin with an underscore (_
), as Chrome will fire off a warning that “Filenames starting with “_” are reserved for use by the system.”

This leads us to have a new build script like this:
"scripts": {
"build": "next build && next export && mv out/_next out/next && sed -i '' -e 's/\\/_next/\\.\\/next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/"
}
sed
on works differently on MacOS than it does on Linux. MacOS requires the '' -e
flag to work correctly. If you’re on Linux you can omit that additional flag.
Assets
If you are using any assets in the public
folder of your Next.js project, we need to bring that into our Chrome extension folder as well. For organization, adding a next-assets
folder inside public
ensures your assets aren’t output directly into the extension
directory.
The full build script with assets is this, and it’s a big one:
"scripts": {
"build": "next build && next export && mv out/_next out/next && sed -i '' -e 's/\\/_next/\\.\\/next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/ && rm -rf out && rsync -va --delete-after public/next-assets ../extension/"
}
Chrome Extension Manifest
The most common pattern for activating a Chrome extension is to trigger a pop-up when the extension is clicked. We can do that in Manifest V3 by using the action
keyword. And in that, we can specify default_popup
so that it points to an HTML file.
Here we are pointing to an index.html
from Next.js:
{
"name": "Next Chrome",
"description": "Next.js Chrome Extension starter",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_title": "Next.js app",
"default_popup": "index.html"
}
}
The action
API replaced browserAction
and pageAction
` in Manifest V3.
Next.js features that are unsupported by Chrome extensions
Some Next.js features require a Node.js web server, so server-related features, like next/image
, are unsupported by a Chrome extension.
Start developing
Last step is to test the updated Next.js Chrome extension. Run npm build
(or yarn build
) from the next-app
directory, while making sure that the manifest.json
file is in the extension
directory.
Then, head over to chrome://extensions
in a new Chrome browser window, enable Developer Mode*,* and click on the Load Unpacked button. Select your extension
directory, and you should be able to start developing!

Wrapping up
That’s it! Like I said, none of this was immediately obvious to me as I was getting started with my Chrome extension rewrite. But hopefully now you see how relatively straightforward it is to get the benefits of Next.js development for developing a Chrome extension. And I hope it saves you the time it took me to figure it out!
Great article. How about creating Firefox extension? Is it similar to what you have done above? Also how about using ‘browser’ extension API to create common extension for all browsers?
Hey Abu! Yes, actually porting a Chrome Extension to Firefox is now very compatible. The one thing to keep in mind though is they currently have different plans for Manifest V3
— Porting a Google Chrome extension
Let’s say if i need content scripts, how can i access the components from within content scripts or is that not possible?
Might be possible but extremely difficult to configure and out of scope for this example. You actually can’t import modules inside
content_scripts
— https://stackoverflow.com/a/58137279/7948880Great article! I found it quite helpful.
Note that in the example you’re missing a few references to
/_next/
, one of which is inside a regex.I’ve created a quick javascript based script that will work for all platforms. See it in action here.
Thanks Weber! I like your script.
Can you clarify what references are missing?
Hi Thomas
FYI, I managed to simplify this a bit by including the manifest.json inside the public folder.
That way, it gets auto-magically copied during build
I woke up from a nap realizing I had to make a chrome plugin. I was super concerned until I saw this. Easy as pie, and I’ve got it up and running! This is fantastic! Thank you!
Amazing post. Thanks a lot!
One thing I would slightly tweak is the build script. It took me a while to understand why links to other pages wouldn’t work in my static export.
It’s because the script only moves the index.html file. To move all pages I would change it to this
Thank you! Here’s a modified version of Webber’s script if you want to use Node.js instead: https://github.com/typefully/minimal-twitter/blob/main/popup/sanitize.js