Introduction
In this blog, we will explore how to set up and develop a Chrome extension using TypeScript, React, Tailwind CSS, and Webpack. We will create a minimal extension called "NoteMe" ✍️ to put our understanding to the test. Our extension will include the following features:
- Allow users to add multiple notes for a given website
- Enable users to view saved notes for a given website
- Provide the option to delete notes for a given website
- Save notes locally in the browser’s storage
- Optionally sync notes with a backend for cloud storage
Refresher
In this blog, we will learn how to build a Chrome extension using modern technologies. This guide assumes that you already have some familiarity with building and uploading an extension to Chrome during local development. If you are new to this or need a detailed walkthrough of the basics, I recommend checking out my previous blog: Link
Extension sneak peek
The extension will include the following components:
- Toggle Button: A button to open and close the sidebar.
- Sidebar: A versatile panel where users can: Write new notes. View saved notes. Delete saved notes. Sync notes with the backend (provision available in the code, though no backend is connected currently).
- Popup: A small window allowing users to reposition the toggle button (used to open/close the sidebar) at prespecified positions on the screen Note: While there’s no backend integration in this implementation, the code includes provisions to connect a backend in the future.
Below are screenshots showcasing how the extension will look upon completion:
Prerequisites
Before diving into this tutorial, ensure you have the following tools installed on your system:
- Node.js (v18.16 LTS or later)
- NPM (Node Package Manager, bundled with Node.js)
- TypeScript
- Webpack
- VS Code Editor (or any code editor of your choice)
Extension from 40,000 Feet
The figure above provides a high-level overview of the internal workings of this extension. Here are some key points we can derive from the diagram:
- The content script interacts directly with the
DOM
of the parent web page, enabling it to modify the page's content. - Popup, background, and content scripts communicate with each other through Chrome's runtime messaging system.
- For tasks related to Chrome storage or backend API calls, content or popup scripts delegate the responsibility to the background worker using the runtime messaging system.
- The background script acts as the sole mediator with the app backend and Chrome's storage. It also relays notifications, if any, to other scripts using runtime messaging.
- Popup and content scripts exchange information directly through Chrome's runtime messaging system.
Setup of the extension
While Chrome extension projects don’t mandate a specific project structure, they do require a manifest.json
file to be located at the root of the build directory. Taking advantage of this flexibility, we’ll define a custom project structure that helps organize different scripts effectively. This structure will enable better code reuse across scripts and minimize duplication, streamlining our development process.
Step 1: Create a basic directory structure for the project
To get started, we’ll set up a foundational directory structure for the project. You can use the following bash script to create the basic structure along with the manifest.json
file:
#!/bin/bash
bash_script_absolute_path=$(pwd)
declare public_paths=("public" "public/assets" "public/assets/images")
declare source_paths=("src" "src/lib" "src/scripts" "src/scripts/background" "src/scripts/content" "src/scripts/injected" "src/scripts/popup" "src/styles")
declare public_directory_path="public"
declare manifest_file="manifest.json"
declare project_name="note-me"
create_directory () {
if [ ! -d "$1" ]; then
mkdir ${1}
fi
}
create_file () {
if [ ! -e "$2/$1" ]; then
touch $2/$1
fi
}
create_public_directories () {
for public_path in "${public_paths[@]}";
do
create_directory $public_path
done
}
create_source_directories () {
for source_path in "${source_paths[@]}";
do
create_directory $source_path
done
}
execute () {
echo "creating project struture at "${bash_script_absolute_path}
create_directory $project_name
cd $bash_script_absolute_path"/"$project_name
create_public_directories
create_source_directories
create_file $manifest_file $public_directory_path
echo "done creating project struture at "${bash_script_absolute_path}" with project name "$project_name
}
execute
Ensure that your directory structure resembles the one shown in the screenshot below.
Step 2: The manifest.json
file located in the public
directory should be structured as shown below:
{
"manifest_version": 3,
"name": "NoteMe",
"version": "1.0",
"description": "A Chrome extension built with React and TypeScript using Webpack.",
"action": {
"default_popup": "popup.html",
"default_icon": "app-icon.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"permissions": [
"storage",
"activeTab",
"scripting",
"webNavigation"
],
"host_permissions": ["<all_urls>"],
"web_accessible_resources": [
{
"resources": ["styles.css", "sidebar-open.png", "sidebar-close.png"],
"matches": ["<all_urls>"]
}
]
}
Points to note:
- The file extensions are
.js
because the.ts
files will be compiled into.js
files, which are required at runtime in the Chrome environment. - The
matches
field uses<all_urls>
as its value, enabling the extension to operate on any webpage loaded in Chrome. - Three image files are referenced:
app-icon.png
,sidebar-open.png
, andsidebar-close.png
. You can find these files in the repository linked at the end of this blog. - The
manifest.json
file must be placed at the root level of thedist
directory after the project is built. To ensure this, we need to configure the webpack settings to move it appropriately during the build process.
Step 3: Initialize npm and Install Dependencies
- Start by initializing npm in your project using the following command:
npm init -y
- Add the necessary development dependencies to the
devDependencies
section of your project. Run the following command:npm i --save-dev @types/chrome @types/react @types/react-dom autoprefixer copy-webpack-plugin css-loader mini-css-extract-plugin postcss postcss-loader style-loader tailwindcss ts-loader typescript webpack webpack-cli webpack-dev-server
- Add the runtime dependencies needed for running the project:
npm i --save react react-dom
Step 4: Create files referenced in the manifest.json
Create following files which are referenced in the manifest.json
: backgroun.ts
, content.ts
and popup.html
.
-
background.ts
: Create this file in thesrc/scripts/background
directory -
content.ts
: Create this file in thesrc/scripts/content
directory -
popup.html
Create this file in thepublic
directory
Step 5: Update popup
and background
Code
Add the following code to the popup.html
file in the public
directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="styles.css" type="text/css" />
<title>Popup</title>
</head>
<body>
<div id="root"></div>
<script src="popup.js"></script>
</body>
</html>
Note:
This file acts as the popup interface that appears when the user clicks on the extension name in the Chrome browser. The styles.css
and popup.js
files will be generated in subsequent steps, so please follow along.
Add the following code to the background.ts
file in the src/scripts/background
directory:
import { onAppInstalled, onMessage } from "../../lib/worker";
(() => {
chrome.runtime.onInstalled.addListener(onAppInstalled);
chrome.runtime.onMessage.addListener(onMessage)
})();
Note:
The code above installs two listeners:
- The function registered by
chrome.runtime.onInstalled.addListener
executes whenever the extension is installed in the browser. This can be used to initialize Chrome storage or a backend (if applicable) with a predefined state. - The function registered by
chrome.runtime.onMessage.addListener
executes whenever the background script receives a message from the content or popup scripts.
Additionally, the import
statement brings in listeners from the src/lib
directory. The core app logic is built in src/lib
, enabling reuse across different contexts (e.g., content
and background
scripts).
Step 6: Walkthrough of the src/lib
Directory
The src/lib
directory houses the core logic of the extension. Below is an overview of its structure and key components:
-
components
Directory: Contains all the React components used in the extension. -
lib/components/ContentApp.tsx
: Acts as the container component for the content script. -
lib/components/NoteMePosition.tsx
: Contains the component responsible for the popup script. -
helpers.ts
: Includes helper functions used throughout the extension. -
storage-model.ts
: Manages interactions with Chrome's local storage. For details about the structure of the data stored, refer to this file along withtypes.ts
. -
types.ts
: Defines the custom types used in the extension. -
worker.ts
: Contains callbacks for background event listeners.
For detailed implementation, please refer to the actual code in the repository.
Step 7: Mounting React Components
In this step, we mount the React components for rendering. These components are mounted in two different scripts:src/scripts/content/content.ts
and src/scripts/popup/popup.ts
.
Popup Script: Found in src/scripts/popup/popup.ts
.
(() => {
const container = document.createElement("div");
container.id = "note-me-position-component-root";
document.body.appendChild(container);
const root = ReactDOM.createRoot(container);
root.render(<NoteMePosition buttons={BUTTONS}/>);
})();
Content Script: Found in src/scripts/content/content.ts
.
export const mountContentRootComponent = () => {
const container = document.createElement("div");
container.id = "note-me-component-root";
const shadowRoot = container.attachShadow({ mode: "open" });
document.body.appendChild(container);
const styleLink = document.createElement("link");
styleLink.setAttribute("rel", "stylesheet");
styleLink.setAttribute("href", chrome.runtime.getURL("styles.css"));
shadowRoot.appendChild(styleLink);
const shadowContainer = document.createElement("div");
shadowRoot.appendChild(shadowContainer);
const root = ReactDOM.createRoot(shadowContainer);
root.render(<ContentApp />);
};
key points:
- Separate Mounting Scripts: The popup and content scripts operate in different contexts
-
Popup Script: Runs within the context of the
popup.html
webpage in which it is loaded. - Content Script: Runs within the context of the main webpage loaded in the browser.
-
Shadow DOM for Content Script:
- Styles injected by the content script could potentially affect the parent webpage's appearance.
- To prevent this, we use the Shadow DOM to encapsulate the styles, ensuring they remain isolated within the extension.
- This is not necessary for the popup script, as it operates in its own isolated environment (
popup.html
).
Step 8: Configurations for Compiling and Building
Adding the configurations required for compiling and building the extension
To successfully compile and build the extension, we need to configure the following files:
postcss.config.js
tailwind.config.js
-
tsconfig.json
webpack.config.js
Key Points:
- Default Settings: Wherever possible, default settings are provided to simplify the process and ensure focus remains on the primary goal—building a fully functional extension.
- Details in Repository: For the complete configurations and detailed settings of these files, please refer to the code repository.
These configurations handle the TypeScript compilation, Tailwind CSS integration, and the overall Webpack build process for the extension.
Testing the extension
-
Generate the
dist
Directory: Run the following command to create thedist
directory:npm run build
-
Upload to Chrome:
- Open Chrome and navigate to
chrome://extensions/
. - Enable Developer Mode in the top-right corner.
- Click on Load Unpacked and select the
dist
directory.
- Open Chrome and navigate to
-
Verify Installation:
- Once loaded, the extension's icon will appear on each page in the bottom-right corner by default.
-
Functionality Check:
- Position Control: Use the controls in the popup to change the position of the icon.
- Notes Feature: Notes are saved independently for each website and can be deleted for a specific site without affecting others.
-
Backend Simulation:
- While there is no backend connected currently, the code includes a provision to integrate with one.
- The current implementation mimics a backend connection using
setTimeout
andpromises
to simulate asynchronous interactions.
Here are some screenshots captured during the testing of the extension.
Key Takeaways
Here are a few key takeaways from this blog,
- We explored how various components of the Chrome environment, such as content scripts, popup scripts, and background workers, communicate with each other using Chrome's runtime messaging system.
- We learned how to configure and build a Chrome extension from scratch, including setting up the project structure, installing dependencies, and writing core functionality.
- We discovered some good practices, such as:
- Enhancing code reusability across scripts for maintainability and scalability.
- Utilizing Shadow DOM in content scripts to prevent style conflicts with the parent webpage.
Glimpse Ahead
In the future, I plan to work on another blog where we will explore the process of publishing a fully functional Chrome extension to the Chrome Web Store. The goal of that blog will be to:
- Develop an extension complex enough to solve a real-world problem.
- Demonstrate the step-by-step process of publishing the extension to the Chrome Web Store.
Thank you for taking the time to read this blog! Your interest and support mean so much to me. I’m excited to share more insights as I continue this journey.
Happy coding!
github link: https://github.com/gauravnadkarni/chrome-extension-starter-app
This article was originally published on Medium.
Top comments (0)