A content script is JavaScript from a Chrome Extension that the browser executes on a designated host page. It shares the DOM with the host page but has a separate JavaScript environment.
If you've done any work on Chrome Extensions, you know that content scripts are one of the essential tools at your disposal. However, you also know they are a PITA to develop.
The traditional content script developer experience is as follows:
- Make changes to your code
- Reload the extension in
chrome://extensions
- Reload the host page
- Check that things work right
- Repeat
Forget a step, and your changes don't show up. Hopefully, you realize what's happening before you start debugging. 😅
Adding DOM elements to a website is a standard content script use case. Unfortunately, you need to bundle your code to use a framework like React or Vue in a content script.
Vite does a great job serving code for the browser, but content scripts load their code from the file system, so Vite's sweet HMR experience doesn't work out of the box. Until now.
Good News for Content Script DX
You can have HMR in content scripts; you can say goodbye to the tedious workflow they represent. My name is Jack Steam, and I'm the creator of the CRXJS Vite plugin. Today CRXJS brings an authentic Vite HMR experience to content scripts for the first time. Let me show you how to get started.
If you're coming from my first article, Create a Vite-React Chrome Extension in 90 seconds, you already know how to get started; you can skip this next bit. Instead, scroll down to the next heading, "Add a content script".
Getting Started
Using your favorite package manager, initialize a new Vite project. Follow the prompts to set up your project. CRXJS works with React and Vue, but we'll use React for this guide.
CRXJS doesn't yet work with Vite 3, but support is coming soon.
npm init vite@^2
cd <your-project-name>
npm install
npm i @crxjs/vite-plugin@latest -D
Open vite.config.js
and add CRXJS:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import { crx } from '@crxjs/vite-plugin'
export default defineConfig({
- plugins: [react()]
+ plugins: [react(), crx()]
})
Chrome Extensions declare their resources using a manifest.json
file. Create your manifest next to vite.config.js
with these fields:
{
"manifest_version": 3,
"name": "Vite React Chrome Extension",
"version": "1.0.0"
}
Go back to vite.config.js
and add the manifest:
// other imports...
+ import manifest from './manifest.json'
export default defineConfig({
- plugins: [react(), crx()]
+ plugins: [react(), crx({ manifest })]
})
Add a content script
We declare content scripts with a list of JavaScript files and match patterns for the pages where Chrome should execute our content script. In manifest.json
, create the field content_scripts
with an array of objects:
{
// other fields...
"content_scripts": [{
"js": ["src/main.jsx"],
"matches": ["https://www.google.com/*"]
}]
}
Here we're telling Chrome to execute src/main.jsx
on all pages that start with https://www.google.com
.
Create the root element
Content scripts don't use an HTML file, so we need to create our root element and append it to the DOM before mounting our React app. Open src/main.jsx
and add a root element.
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
+ const root = document.createElement('div')
+ root.id = 'crx-root'
+ document.body.append(root)
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
- document.getElementById('root')
+ root
)
Get the right URL
Content scripts share the origin of their host page. We need to get a URL with our extension id for static assets like images. Let's go to src/App.jsx
and do that now.
<img
- src={logo}
+ src={chrome.runtime.getURL(logo)}
className="App-logo"
alt="logo"
/>
Now our content script is ready for action! But, first, let's load our extension in Chrome.
Load the extension
Start Vite in the terminal.
npm run dev
Open the extensions dashboard at the URL chrome://extensions
and turn on development mode using the switch in the top right corner. Next, load your extension by dragging the dist
folder onto the extensions dashboard.
Profit with HMR
Navigate to https://www.google.com
and scroll to the bottom of the page. There's our familiar Vite Hello World!
Notice how the counter button doesn't look like a button. That's because Google's styles affect our content script elements. The same goes the other way: our styles change Google's styles.
Let's fix that. Replace everything in src/index.css
with this:
#crx-root {
position: fixed;
top: 3rem;
left: 50%;
transform: translate(-50%, 0);
}
#crx-root button {
background-color: rgb(239, 239, 239);
border-color: rgb(118, 118, 118);
border-image: initial;
border-style: outset;
border-width: 2px;
margin: 0;
padding: 1px 6px;
}
CRXJS will quickly rebuild the content script, and our CSS changes will take effect. Now our div
position is fixed, and that button looks more like a button! Click the count button and play around with src/App.jsx
to see Vite HMR at work.
We need your feedback! If something doesn't work for you, please create an issue.
Conversely, if CRXJS has improved your developer experience, please consider sponsoring me on GitHub or give me a shoutout on Twitter. See you next time.
Good luck building your Chrome Extension!
Top comments (13)
Great!
how to work with background.js for service worker? you should include it in the doc
Great idea! We're working on the docs, you can take peek here: crxjs.dev/vite-plugin
Check out this comment for instructions on the background service worker: dev.to/lichenglu/comment/1od4p
I checked, I did the same in maniest json. this code will work fine inside it?
I am actuallyy trying to inject the content script by clicking on the icon of extension. so, kindly tell me how it will work, I tried with this code. but It throw error at this line
You can import the script with a script query:
This will also provide HMR for the imported script. More info: Dynamic content scripts
Vite + vue, css is not applied.
github.com/keyding/vite-crx-vue-test
Thanks for creating an issue! I'm glad we were able to get things resolved.
I see this error
when I try to add:-
"content_scripts": [
{
"js": ["src/main.jsx"],
"matches": ["google.com/*"]
}
]
Any solutions for this?
This is a known issue with the new Vite 3.0, but you can install the latest versions of Vite 2 and plugins and things should work as expected:
I'm updating the articles so this doesn't continue to trip people up.
Hi Jack, This resolved the issue for content script hot reloading but the pop up always shows Waiting for service worker..
and this is the Error shown in Brave extension page
I just spent 2 days of trying to develop a content script with Webpack and HMR with no luck. Thank you!! It works like a charm 🪄
I need information on how to setup content script with Tsx. thank you