DEV Community

Cover image for Chrome Extension Development - Develop minimal app with TypeScript, React, Tailwind CSS and Webpack
Gaurav Nadkarni
Gaurav Nadkarni

Posted on • Originally published at Medium

Chrome Extension Development - Develop minimal app with TypeScript, React, Tailwind CSS and Webpack

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:

Screenshot of the app running with facebook.com

Screenshot of the app running with google.com

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

control flow diagram of the chrome extension

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
Enter fullscreen mode Exit fullscreen mode

Ensure that your directory structure resembles the one shown in the screenshot below.

Screenshot showing directory structure

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>"]
      }
    ]
  }
Enter fullscreen mode Exit fullscreen mode

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, and sidebar-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 the dist 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 the src/scripts/background directory
  • content.ts: Create this file in the src/scripts/content directory
  • popup.html Create this file in the public 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>
Enter fullscreen mode Exit fullscreen mode

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)
})();
Enter fullscreen mode Exit fullscreen mode

Note:

The code above installs two listeners:

  1. 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.
  2. 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:

Screenshot showing directory structure of the lib directory

  • 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 with types.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}/>);
})();
Enter fullscreen mode Exit fullscreen mode

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 />);
};
Enter fullscreen mode Exit fullscreen mode
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:

  1. postcss.config.js
  2. tailwind.config.js
  3. tsconfig.json
  4. 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

  1. Generate the dist Directory: Run the following command to create the dist directory:  npm run build
  2. 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.
  3. Verify Installation:
    • Once loaded, the extension's icon will appear on each page in the bottom-right corner by default.
  4. 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.
  5. 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 and promises to simulate asynchronous interactions.

Here are some screenshots captured during the testing of the extension.

Screenshot showing extension running with google.com

Screenshot showing extension running with yahoo.com

Screenshot showing object structure stored in the chrome storage

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)