Often overlooked yet incredibly powerful, Chrome Extensions serve as versatile tools to enhance and personalize the Browse experience.
In this article, we will delve into the creation and functionality of an extension called “Inverse Dark/Light Mode.” This small yet effective tool demonstrates how extensions can address daily needs and offer a wide range of functionalities, showcasing the potential for significant user experience improvements.
Overview of the “Inverse Dark/Light Mode” Extension
The “Inverse Dark/Light Mode” extension was conceived to offer dynamic control over web content display. Its primary features include:
- Inverse Mode: The ability to instantly transform a website from “light mode” to “dark mode” and vice versa, providing customizable visual comfort.
- Options Page: A dedicated section for managing the extension, where users can view and modify the list of websites for which “inverse mode” is active.
- Text Counter: An additional feature that counts the number of words or characters in selected text, accessible via a custom entry in Chrome’s context menu.
Let’s dive into the technical details of each of these functionalities.
The “Inverse Mode” and its Visual Effects
The core of this extension lies in its ability to invert the colors of a web page. The underlying concept is straightforward yet effective: apply dynamic CSS styles to modify the site’s appearance.
The Idea Behind Color Inversion
The primary mechanism relies on injecting CSS styles upon page load. Specifically, two fundamental CSS properties are used:
filter: invert(1)
: Applied to the<html>
element of the page, this filter inverts all colors on the site. White becomes black, black becomes white, and every color transforms into its complementary hue. This is the initial step to achieve the desired theme change.filter: hue-rotate(180deg)
: This filter is applied to restore the original colors of specific elements such as images, videos, and iframes. Whileinvert(1)
is excellent for text and backgrounds, applying it to an image or video would render their colors unnatural. Rotating the hue 180 degrees compensates for the chromatic inversion of these elements, restoring them to a more visually accurate representation.

It is important to note that inverting colors means transforming every color into its complementary. For example, white will become black, red will become cyan, and so on. The hue-rotate(180deg)
filter specifically intervenes to mitigate this undesired effect on certain elements, attempting to restore colors to their original state.

Why Don’t All Colors Return Perfectly Original?
This is a crucial point for understanding the limitations of CSS filters. The hue-rotate()
filter manipulates the hue (the “H” in the HSL color model), but color rendering on the screen happens in RGB. When a color is declared in HSL in CSS, the browser internally converts it to RGB for display.
CSS filters like invert(), hue-rotate(), brightness(),
etc., operate after visual rendering. This means they do not work on the color values declared in CSS but rather on the final rendered image, i.e., on the RGB pixels.
Consequently, hue-rotate()
simulates a hue rotation in the HSL model, but the processing occurs approximately on the RGB model. The browser performs a conversion from RGB to HSL, rotates the hue, and then converts back to RGB. This double conversion and approximation in the RGB model can lead to slight deviations, which is why some colors might not return precisely to their original state after the combined application of invert(1)
and hue-rotate(180deg)
.
Loading Styles and Scripts

To ensure these styles and scripts are loaded at the opportune moment, the extension’s manifest.json file plays a crucial role. Through the content_scripts
key, the loading of style.css and content.js
(the latter for interface logic) is configured, ensuring they are injected into visited web pages. Simultaneously, service-worker.js
is loaded to manage the extension’s background logic.
Activation, Deactivation, and State Persistence

Upon clicking the extension icon, the logic implemented in service-worker.ts
is triggered. A CSS class (inverse-mode)
is added to or removed from the <html>
tag of the current page. This class, in turn, enables or disables the previously described CSS rules.
For a seamless user experience, the extension leverages LocalStorage to save the list of website origins (e.g., https://danielzotti.it
if visiting https://danielzotti.it/blog
) for which “inverse mode” has been activated. This ensures that the state of “inverse mode” persists during navigation within the same domain or when returning to the site in the future, without requiring manual re-activation.


Handling Additional Events for a Consistent Experience
Keeping the extension icon and the .inverse-mode
class aligned with the page’s current state goes beyond a simple icon click or LocalStorage changes. It is crucial to manage a series of additional scenarios to ensure a smooth and consistent user experience:
- Tab Change: When the user switches between tabs, the extension icon must reflect the state (active/inactive) of “inverse mode” for the newly selected tab’s domain.
- New Tab Opening: If a new tab is opened, the extension must check if the domain is present in the list of sites with “inverse mode” active. If so, the
.inverse-mode
class must be added to the HTML tag, and the icon updated.
To address these cases, the extension utilizes several Chrome APIs:
chrome.webNavigation.onCommitted:
This event is fired when a navigation has been definitively committed, meaning the browser has decided to load a new page in a tab. It is used to inject thecontent.js
file, which is essential for applying styles and frontend logic.chrome.runtime.onStartup:
Executed when Chrome starts (not on extension installation or update), this event is useful for identifying the active tab and updating the extension icon image from the beginning of the Browse session.chrome.tabs.onUpdated
: Called every time a tab is updated (URL change, status, redirect, etc.). It is used to update the icon image based on the tab’s state.chrome.tabs.onActivated
: Activated when the user changes the active tab. This event is crucial for adding the.inverse-mode
class to the new tab’s HTML tag and for updating the extension icon in real-time.
Necessary Permissions: The manifest.json

Every Chrome extension requires a manifest.json
file that declares the permissions needed to operate correctly. For “Inverse Dark/Light Mode,” essential permissions include:
scripting
: Allows the extension to inject and execute JavaScript and CSS code into web pages, fundamental for “inverse mode.”tabs
: Permits obtaining information about open tabs in the browser, such as URL and title, useful for managing states and icons.activeTab
: Provides temporary access to the active tab when the user interacts with the extension (e.g., clicking the icon). It is a more limited permission thantabs
and does not require explicit authorization during installation.storage
: Enables the extension to store data locally using thechrome.storage
API, indispensable for saving preferences for sites with “inverse mode” active.webNavigation
: Allows monitoring user navigation, crucial for executing thecontent.js
script whenever a new page is loaded or a URL changes, ensuring dynamic application/removal of the .inverse-mode
class.contextMenus
: Allows adding custom entries to the context menu (right-click), enabling the bonus text counting functionality.
The “Options” Page: The Control Center
Extensions often include an “Options” page, a user interface dedicated to configuration and settings management. In our extension, this page serves to display and manage the list of sites for which “inverse mode” has been activated.
For the development of this page, a modern framework stack including React 19, TypeScript, SCSS, and Vite was utilized. While this might seem like an “overkill” approach for a simple options page, this choice was made to explore the integration capabilities of these technologies in Chrome extension development.
To access the “Options” page, simply right-click the extension icon in Chrome’s toolbar and select the “Options” entry.

Activating the “Options” Page
The activation of the “Options” page occurs in the manifest.json by specifying the HTML file that serves as the interface for the extension’s options.

Vite Configuration for the Build
Given the use of React and multiple files for different extension functionalities (background scripts, styles, options page), a specific Vite configuration was necessary. This build tool was configured to separately generate the files for the React application (the options page) and other files like style.css, content.js, and service-worker.js
, optimizing the compilation and deployment process.

Counting Words and Characters with the Context Menu
As an additional bonus, the extension includes a feature to count the number of words or characters in selected text directly from a Chrome context menu entry. The objective is straightforward:
- Add an entry to Chrome’s context menu, under the extension’s name (“Inverse Dark/Light Mode”).
- When the user selects text on the page, right-clicks, and chooses the “Count Words” entry, a simple alert will display the total number of words in the selected text.

Adding the Item to the Context Menu
This functionality is implemented in service-worker.ts
, where the specific entry is added to the context menu using the chrome.contextMenus API
.

Word Counting
Once the user selects “Count Words” from the context menu, service-worker.ts executes the counting logic. The selected text is acquired, and using a carefully defined regular expression, words are identified and counted. The result is then displayed in an alert.

Want to See the Code?
The entire project is available on GitHub, where you can explore the complete source code and delve into every implementation aspect:
https://github.com/danielzotti/chrome-extension-react-typescript-vite
If you are interested in trying the extension directly, you can download it from the Chrome Web Store:
https://chromewebstore.google.com/detail/inverse-darklight-mode/bddlmhjdondkdinihlncinlgeddemidc
Main Author: Daniel Zotti, Team Lead Front-end @ Bitrock