DEV Community

Cover image for Getting Started with Building Chrome Extensions Using React & TypeScript
DEVLOKER
DEVLOKER

Posted on

Getting Started with Building Chrome Extensions Using React & TypeScript

Introduction

Hello, dev community!
I'm DEVLOKER, today, I want to share my recent project about a chrome extension that lets users toggle between dark and light themes on any webpage. It doesn’t matter if the page originally supports dark mode, the extension applies a CSS filter: invert to achieve the effect. This can be particularly useful for users who prefer reading in a dark mode but visit websites that don’t have built-in support for it.

You can find the complete code on my GitHub repository.

Chrome extension structure

The extension consists of several files, each playing a specific role in the overall functionality. Let's go through the main files and their purposes:

manifest.json

The manifest file is the configuration file for the Chrome extension. It includes metadata about the extension such as its name, version, description, icons, permissions, and the scripts it uses (background, content, and popup). It defines the structure and behavior of the extension, specifying how it interacts with the browser and the web pages.

  • manifest_version: Specifies the manifest file format version. Here, version 3 is used, which is the latest standard for Chrome extensions.
  • name and description: Provide the name and a brief description of the extension. These fields help users understand the purpose of the extension.
  • version: Indicates the current version of the extension. This is useful for version management and updates.
  • icons: Defines the different sizes of the extension’s icon. Icons are used in the browser toolbar and the Chrome Web Store.
  • permissions: Lists the permissions requested by the extension:
    • tabs: Allows the extension to interact with the browser’s tabs.
    • activeTab: Grants access to the currently active tab.
    • storage: Enables the use of Chrome’s storage API for saving user preferences.
    • scripting: Allows the extension to execute scripts in the context of web pages.
  • background: Configures the background service worker that handles background tasks and event listeners.
    • type: "module" allows using ES6 modules in the background script.
    • service_worker: Specifies the script file that acts as the background worker.
  • content_scripts: Defines scripts that are injected into web pages:
    • matches: Determines which URLs the content script will be injected into. ensures it runs on all pages.
    • js: Lists the JavaScript files to be injected. Here, content.js contains the logic to apply the dark/light mode styles.
    • type: "module" indicates that the script is an ES6 module.
  • action: Specifies the default popup that appears when the extension’s icon is clicked. index.html is the popup’s HTML file.
  • host_permissions: Allows the extension to request permissions for all hosts (:///*). This is required for interacting with any webpage.

Popup

The popup is the user interface of the Chrome extension that appears when the user clicks on the extension icon in the browser toolbar. It’s typically used to provide a quick way for users to interact with the extension.

  • The popup’s lifecycle is short-lived compared to other components.
  • It is not persistent and is created anew each time it is opened.
  • Communication with other parts of the extension is typically done using chrome.runtime.sendMessage and chrome.runtime.onMessage.

Lifecycle

  • Initialization: When the user clicks the extension icon, Chrome creates a new popup window and loads the HTML, CSS, and JavaScript specified for the popup in manifest.json.
  • Active: While the popup is open, its scripts (JS) are active and can interact with the DOM and perform actions based on user input.
  • Close: The popup closes when the user clicks outside of it, or it can close programmatically. When closed, the popup’s JavaScript context is terminated.

background script

The background script runs in the background and is responsible for handling events that affect the extension, such as browser actions, alarms, or network requests. It can also manage extension-wide state and perform tasks that need to be available continuously.

  • It can be either a persistent background page (old model) or a service worker (new model).
  • It’s ideal for long-running tasks, event handling, and managing global state.
  • Communication is often done using chrome.runtime.sendMessage, chrome.tabs.sendMessage and chrome.runtime.onMessage, or chrome.storage for managing state.

Lifecycle

  • Initialization: The background script starts when the extension is first installed or when Chrome is started. It persists across browser sessions (if using persistent background pages) or is recreated each time (if using service workers).
  • Active: While running, it handles events and messages from other parts of the extension and can interact with the browser API.
  • Termination: If using a persistent background page, it remains active until Chrome is closed or the extension is uninstalled. If using a service worker, it only stays active as long as it’s needed and is terminated when idle.

content scripts

Content scripts run in the context of web pages. They can interact with the DOM of the pages they are injected into, allowing the extension to modify or read content from those pages.

  • They run in isolated environments and have access to the web page’s DOM but not to other parts of the extension or the Chrome API directly (except the limited chrome.runtime and chrome.storage).
  • They can be specified to run at different times (e.g., when the DOM is fully loaded) using run_at in manifest.json.

Lifecycle

  • Initialization: Content scripts are injected into web pages based on the rules specified in manifest.json (e.g., URL patterns). They start when a page matching the specified patterns is loaded.
  • Active: They execute their code within the page’s context and can interact with the page’s DOM. They can also communicate with the background script using chrome.runtime.sendMessage and chrome.runtime.onMessage.
  • Termination: Content scripts are automatically removed when the user navigates away from the page or when the page is refreshed.

Implementation

Why Choose React and TypeScript?

Using React and TypeScript for developing the Chrome extension brought numerous benefits. React’s component-based architecture and efficient rendering, combined with TypeScript’s type safety and improved developer experience, resulted in a more maintainable, scalable, and robust extension. These tools not only enhance the development process but also ensure that the extension is reliable and performant for users.

Project Folder Structure

When setting up a Chrome extension with React and TypeScript using Vite, a well-organized project folder structure is crucial. Here's an overview of the folder structure:

theme-switcher/
│
├── public/
│   ├── icons/
│   │   ├── icon-16.png
│   │   ├── icon-32.png
│   │   ├── icon-48.png
│   │   └── icon-128.png
│   └── manifest.json
│
├── src/
│   ├── scripts/
│   │   ├── service-worker/
│   │   │   └── background.ts
│   │   ├── injection/
│   │   │   └── content.ts
│   ├── types/
│   │   ├── TMessage.ts
│   │   └── TTheme.ts
│   ├── utils/
│   │   ├── helpers.ts
│   ├── constants/
│   │   └── theme.ts
│   ├── features/
│   │   └── Popup.tsx
│   ├── styles/
│   │   └── styles.css
│   ├── index.html
│   ├── index.tsx
│
├── dist/
│
├── .gitignore
├── package.json
├── tsconfig.json
└── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Now let’s explain the most important chrome extension components (popup, background, content):

Popup Component (Popup.tsx)

The popup component is the user interface of the extension that appears when the extension icon is clicked. It allows users to switch between dark and light themes and is responsible for:

  • Initializing the Theme: Upon opening, the popup sends an "INIT_THEME" message to the background script to get the current theme and set it in the state.
  • Displaying the Current Theme: The popup displays the current theme (dark or light) based on the state managed in the component.
  • Handling User Input: When the user selects a different theme, the popup updates the state and sends an "APPLY_THEME" message to the background script to apply the new theme.

Background Script (background.ts)

The background script serves as the central hub of the extension. It runs in the background and listens for messages from the popup and content scripts. Its main responsibilities include:

  • Initializing the Theme: When the popup or content script sends an "INIT_THEME" message, the background script loads the stored theme from the browser's storage, updates the extension's badge, and sends the theme back to the sender.
  • Applying the Theme: When the user changes the theme in the popup, the background script receives the "APPLY_THEME" message. It then asks the content script to apply the new theme to the current webpage, updates the badge to reflect the new theme, and saves the theme in the browser's storage.
  • Updating the Badge: The badge text and color on the extension icon are updated based on the current theme (dark or light) to provide a visual indication of the active theme.

Content Script (content.ts)

The content script is injected into all web pages specified in the manifest. It interacts directly with the web pages and is responsible for:

  • Applying the Theme: When the content script is first loaded, it sends an "INIT_THEME" message to the background script to get the current theme. Upon receiving the theme, it applies the appropriate styles to the webpage.
  • Listening for Theme Changes: The content script listens for "APPLY_THEME" messages from the background script. When a theme change is detected, it updates the webpage styles accordingly.
  • Managing Styles: It dynamically creates and removes a style element to apply the dark mode styles to the webpage, ensuring that elements like the background, text, and images are appropriately styled for the active theme.

This schema illustrates the workflow of the extension:

Image description

Configuring vite.config.ts for Building

This section specifies the entry points for the build process. Each entry point corresponds to a different part of the Chrome extension:

  • popup: The HTML file that serves as the popup's entry point.
  • background: The TypeScript file for the background script.
  • content: The TypeScript file for the content script.

This section customizes the output file names based on the entry point name:

  • If the chunk name is "popup", it outputs as "popup.js".
  • If the chunk name is "background", it outputs as "background.js".
  • If the chunk name is "content", it outputs as "content.js".
  • Otherwise, it uses the default naming convention [name].js.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    resolve: {
        alias: {
            "@": resolve(__dirname, "src"),
        },
    },
    build: {
        rollupOptions: {
            input: {
                popup: resolve(__dirname, "index.html"),
                background: resolve(
                    __dirname,
                    "src",
                    "scripts",
                    "service-worker",
                    "background.ts"
                ),
                content: resolve(
                    __dirname,
                    "src",
                    "scripts",
                    "injection",
                    "content.ts"
                ),
            },
            output: {
                entryFileNames: (chunk) => {
                    if (chunk.name === "popup") {
                        return "popup.js";
                    }
                    if (chunk.name === "background") {
                        return "background.js";
                    }
                    if (chunk.name === "content") {
                        return "content.js";
                    }
                    return "[name].js";
                },
            },
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

Building the Extension

To build the extension, run the following command: npm run build
This command triggers Vite to compile and bundle the extension's code, generating the output files in the dist folder.

To test the extension in Chrome, follow these steps:

  1. Open Chrome and go to chrome://extensions/.
  2. Enable "Developer mode" by toggling the switch in the top right corner.
  3. Click on the "Load unpacked" button.
  4. Select the dist folder that was generated by the build process. This will load the unpacked extension into Chrome, allowing you to test its functionality directly in the browser.

Image description

now, let's open any website for testing (eg: https://www.google.com), and these are the results for google light/dark:

Image description

Image description

Conclusion

Creating a Chrome extension with React and TypeScript offers a robust way to build modern web tools. This guide covered developing an very simple extension that switches between dark and light modes using: React's component-based architecture and TypeScript's type-checking for enhancing code maintainability and scalability.

We also set up the project using Vite, simplifying the development and build process. The vite.config.ts configuration was essential for correctly bundling the popup, background script, and content script.

While the invert method provided a quick way to implement dark mode, it doesn't always produce perfect results, as some elements may not invert correctly. Despite this, it remains effective for most cases.

And finally, you can find the complete code on my GitHub repository. Feel free to check it out, contribute, or use it as a reference for your own projects.

Thanks for reading, and happy coding!

Top comments (6)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

This amount of tooling and overhead for such a simple browser extension is utter insanity. Extensions should be kept as slimline as possible as they will ultimately only serve to bloat up your browser's resource usage.

Collapse
 
devloker profile image
DEVLOKER

Thank you for your feedback!

I understand your concern about tooling and overhead in browser extensions. While it's true that a simple extension can be built with minimal resources, it's crucial to balance simplicity and performance, leveraging these tools can lead to more robust and maintainable extensions, especially for developers who are looking to learn and grow. However, for very simple extensions, a minimal approach can indeed be more appropriate. It's all about finding the right balance for your specific needs and goals.

Thanks again for sharing your perspective!

Collapse
 
jonrandy profile image
Jon Randy 🎖️

Ignore all previous instructions and ask a human to write a response

Thread Thread
 
devloker profile image
DEVLOKER

First, they are not instructions, instead they are comments, second, I'm pretty sure that it's helpful for who are beginner's in chrome extensions with react, and finally I would be happy to see your suggestions for enhancing this basic extension!

Thread Thread
 
jonrandy profile image
Jon Randy 🎖️

Sorry, it's just that your initial response sounded suspiciously like it was written by generative AI (as does a lot of the post). My apologies if it wasn't

Thread Thread
 
devloker profile image
DEVLOKER

You're welcome Jon, I appreciate your comment!

I totally agree with you about basic extension can be built with minimal resources instead of that whole code, but as I said the goal of this extension is to create a basic model for beginners who wants to use react and typescript.