to customize pages. I also wanted to use this opportunity to play around with TypeScript since I haven’t had the chance to use it in a while.
1 — Set Up the Project
Let’s kick things off by creating a new directory for our project and initializing it with npm:
mkdir ts-chrome-extension
cd ts-chrome-extension
npm init -y
2 — Let’s add TypeScript
npm install --save-dev typescript
Once installed, we need to create a TypeScript configuration file. This file will tell the TypeScript compiler how to behave. Let’s create the tsconfig.json file:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es2015"],
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
More details on the tsconfig.json file can be found in the .
5 — The Popup Files
Since we’re not using React yet, the popup will be simple and handle basic DOM interactions.
5.1 —popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body>
<div>
<h1>Hello, Chrome Extension!</h1>
<button id="alertButton">Click me</button>
</div>
<script src="popup.js"></script>
</body>
</html>
This is a simple HTML page with a button and a script reference to popup.js (which will be bundled from popup.ts).
5.2 — popup.ts
This file will handle basic interaction for the popup:
document.addEventListener('DOMContentLoaded', () => {
const alertButton = document.getElementById('alertButton');
if (alertButton) {
alertButton.addEventListener('click', () => {
alert('Button clicked!');
});
}
});
This simple script waits for the DOM to load, then adds a click listener to the button that triggers an alert. This structure sets the foundation for the popup functionality before we add React.
6 — Compile TypeScript
Let’s compile the TypeScript files into JavaScript. Run the following command:
npx tsc
This will transpile your TypeScript files into JavaScript. The popup.ts, background.ts, and contentScript.ts will be compiled to popup.js, background.js, and contentScript.js, respectively. But since we’ll soon add Webpack to manage the bundling, this is just a temporary step.
7 — Adding webpack for Bundling
Now that we have the basic structure, we need Webpack to handle bundling all our scripts into optimized files that Chrome can load. It can handle the copying of icons, transpiling ts into js, automatically injecting script or style files and much more.
7.1 — Install Webpack and necessary loaders
npm install --save-dev webpack webpack-cli ts-loader html-webpack-plugin mini-css-extract-plugin css-loader postcss postcss-loader copy-webpack-plugin
webpack: The core bundler.
ts-loader: Transpiles TypeScript for Webpack.
html-webpack-plugin: Helps generate HTML files with injected script tags.
CSS and PostCSS Loaders: For handling CSS (we’ll use these later with Tailwind).
CopyWebpackPlugin: The CopyWebpackPlugin ensures that the manifest.json and any other assets (like icons) are copied from the src folder to the dist folder.
— We define patterns, such as copying manifest.json from src/ to the root of the dist/ directory.
— If you have icons or other assets (e.g., images, fonts), you can add similar patterns.
7.2 — Webpack Configuration
Next, we need to configure Webpack to bundle everything. Create webpack.config.js at the root of your project:
const path = require('path'); // <-- This is missing and causes the ReferenceError
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: {
popup: './src/popup/popup.ts',
background: './src/scripts/background.ts',
contentScript: './src/scripts/contentScript.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new HtmlWebpackPlugin({
filename: 'popup.html',
template: 'src/popup/popup.html',
chunks: ['popup'],
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'src/manifest.json', to: 'manifest.json' },
// { from: 'src/icons', to: 'icons' }, // Copy any additional assets
],
}),
],
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? false : 'inline-source-map', // Disable source maps in production
};
};
Explanation:
Entry Points: We’ve specified the entry points for popup.ts, background.ts, and contentScript.ts.
Output: The bundled files will be output to the dist folder.
Loaders: We use ts-loader to handle TypeScript and css-loader/postcss-loader For CSS processing (later on when we introduce Tailwind CSS).
HtmlWebpackPlugin: This plugin automatically injects the bundled popup.js file into the popup.html file, ensuring that our scripts are correctly linked.
7.3 — Build the Extension with Webpack
Let’s bundle everything using Webpack by running:
npx webpack --mode development
This will output the bundled files into the dist directory.
If you are having issues because of the icons section, you can either remove it or put some placeholder .pngs for now.
You will have something like this
Isn’t that better?
9 — Adding React to the Popup
We’ve got a functional extension with Tailwind styling and TypeScript for some syntax niceties and early problem detection. We also even have postcss and autoprefixer, which I’ll cover in a different article. And everything works.
Imagine however you need to make the UI more complex to manage loading and unloading data, handling tabs, and showing different UI based on the user’s subscription status. We might be getting ahead of ourselves, but for the sake of this guide, let’s assume there is a reason for it and add React to solve all of these problems. In my use case I added React to handle loading a dynamic set of data (which could’ve been done as a simpler template, but in combination with color pickers and switches of the UI based
9.1 — Install React and ReactDOM
npm install react react-dom @types/react @types-react-dom
9.2 Update Webpack for React
We need to update Webpack so it can handle .tsx files (React with TypeScript). Remember, this was the main goal we set up to do.
To handle this, modify the webpack.config.js file to include React as an entry point:
entry: {
popup: './src/popup/popup.tsx', // Update this to point to the new React component
...
},
resolve: {
extensions: ['.ts', '.tsx', '.js'], // Add .tsx to resolve for React files
},
module: {
rules: [
{
test: /\.tsx?$/, // Handle both .ts and .tsx files
...
9.3 — Convert popup.ts to React
Now we’re going to convert popup.ts to a React component. Rename popup.ts to popup.tsx and update it with a basic React component:
import React from 'react';
import { createRoot } from 'react-dom/client';
import '../styles/tailwind.css';
const Popup = () => {
const handleClick = () => {
alert('Button clicked!');
};
return (
<div className="flex flex-col items-center bg-gray-100 p-4">
<h1 className="text-xl font-bold mb-4">Hello, React Chrome Extension!</h1>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleClick}
>
Click me
</button>
</div>
);
};
const container = document.getElementById('root')
const root = createRoot(container as HTMLDivElement)
root.render(<Popup />);
This simple React component mirrors the functionality of the original popup.ts but uses React’s onClick event for the button.
9.4 — Update popup.html
We don’t need to change much in popup.html except ensure it has the proper div to mount our React component. Here’s the updated popup.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body class="bg-gray-100 w-80">
<div id="root"></div>
</body>
</html>
9.5 — Update tsconfig.json
We need to add "jsx": "react" and "moduleResolution”: “node"to our tsconfig.json It should look like this:
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es2015"],
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react",
"moduleResolution": "node"
},
"include": ["src/**/*"]
}
9.6 — Rebuild with Webpack
Once the changes are made, run Webpack to rebuild the extension -> npx webpack --mode development
Now, when you reload the extension in Chrome and open the popup, you should see a React-powered UI styled with Tailwind CSS.
Conclusion — Why Not Use Create-React-App?
CRA makes a lot of assumptions about how you’re building your app, bundling everything into a single page with dynamically injected JS. This approach works great for a React web app but doesn’t mesh well with Chrome extensions, which use separate HTML pages for the popup, background scripts, and sometimes even options pages.
The best parts of building without CRA? Control and learning.
You decide what gets included, how the bundling works, and how lean or rich your extension becomes.
I also believe it is a good idea to try and see how difficult something is without the library, framework, or bootstrapping helpers. It helps learn a bit more about all of the technologies that are being used indirectly and hidden from you.
If you have gotten this far, I thank you and I hope it was useful to you! Here is a cool image of a cat as a thank you!
SOCIAL SHARE CARD GENERATOR