How to build a chrome extension in 2024
Posted on May 26, 2024 • 9 min read • 1,727 wordsThis is a walkthrough on creating the 'Scrolling Zombie' chrome extension which tracks the amount of scrolling a user does to make them aware of their browsing behavior and help them fight dark UI patterns.
You can find this extension in the Chrome Web Store .
The Internet is now filled with attention vampires and other UI dark patterns. Many websites use these to capture your attention and keep it captive so that they can monetize it. A fair trade if you benefit from it but those practices aim to extract more than what they give.
One of those dark patterns is the “infinite scroll”. You experience it on LinkedIn, Amazon, Facebook, Instagram, TwitteX, etc. Ultimately, it is your choice how much time you want to spend scrolling through those websites. But faced with those tricks, your brain does not stand a chance, It needs a little help.
We’ll use the term Netizen to describe an internet user browsing websites.
As a Netizen, I want to be notified when I have scrolled too much so that I can decide that I should do something else.
As a Netizen, I want to see which sites I spend my time scrolling through so that I can evaluate the value I have gained in comparison to the time I spent.
As a Netizen, I want to decide how much scrolling represents too much scrolling so that I can be notified at a rate in line with my goals and expectations.
As a Netizen, I want to be notified of a crossed threshold while I am scrolling so that I can interrupt the behavior if it does not benefit me anymore.
As a Netizen, I want to be able to reset the stats so that I can measure my activity forward.
Leverage scroll events that are triggered by the browser when the user scrolls. Calculate time deltas and aggregate times between scrolling events. The scrolling information will be stored locally and displayed in the extension’s popup UI. The configuration will be stored locally, in the browser. A notification will be generated each time the accumulated scrolling time for a given page is greater than the sum of the configured threshold and the last notification.
The data captured by the plugin is limited to the minimum No data leaves the user’s computer.
Sensible defaults when possible, for example for the initial Threshold setting.
A failure of the extension should not interfere with the browsing experience. A failure to register some scroll events can be ignored as accuracy is not critical. An order of magnitude is what we are trying to observe.
Performance should not visibly degrade as the number of website tracked increases Storage is limited to 10MB by default which is more than enough for tracking pairs of website titles and integers durations (stored as strings).
Keep the computation to a minimum Use asynchronous operations whenever possible Performance should not visibly degrade as stats table grows
In the context of a google extension, there is a set architecture to follow. We will only leverage a subset of all capabilities.
When analyzing the set of use cases, we see a few components emerge. When we place them in a chrome extension context, we get the following:
The domain is very simple, the only interesting part might be the messages that will be exchanged between the components.
The Service Worker acts as the business logic service, counting and storing times.
The Content-Script injects a probe to relay scrolling events from the pages to the service worker.
The Popup is used to set the threshold setting.
The Popup is used to set the threshold setting.
The deployment is straightforward either through the Chrome Web Store or using offline deployment on the platforms that support it.
In this context, this means that the extension only has the permissions necessary to perform its job.
No browsing data will be sent from the user’s computer. We will store everything in local storage.
├── css
│ └── ...
├── fonts
│ └── ...
├── images
│ ├── icon128.png
│ ├── icon16.png
│ ├── icon32.png
│ └── icon48.png
├── popup
│ └── stats.html
├── scripts
│ ├── scroll_events_forwarder.js
│ ├── stats_service_worker.js
│ └── ...
└── manifest.json
{
"manifest_version": 3,
"name": "Scrolling-Zombie",
"description": "Be aware of the time you spent scrolling on those infinity-scroll websites",
"version": "1.0",
"action": {
"default_popup": "popup/stats.html"
},
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"content_scripts": [
{
"js": ["scripts/scroll_events_forwarder.js"],
"matches": [
"<all_urls>"
]
}
],
"background": { "service_worker": "scripts/stats_service_worker.js", "type": "module" },
"permissions": [
"storage",
"notifications"
]
}
The following configures what html page will be loaded when the user clicks on the extension’s icon (action button)
...
"action": {
"default_popup": "popup/stats.html"
},
...
In our case, that’s the ‘Statistics’ view of the extension and a couple actions (configure threshold, clear stats)
Here’s the source for reference:
<html>
<head>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div id="info">
<h1>Scrolling Zombie!</h1>
<div id="buttons">
<input id="clear" type="button" value="Clear stats!" />
</div>
</div>
<div id="stats">
<p id="threshold">Notify me every <input id="threshold-seconds" type="number" min="5" max="99999" value="60" step="5"/> seconds of active scrolling.</p>
<p id="caption"><b>This is how much you scrolled!</b></p>
<table id="stats-table">
</table>
</div>
<div id="footer">
<p>That's how they get you.</p>
</div>
<script type="module" src="/scripts/popup.js"></script>
</body>
</html>
The scroll_events_forwarder.js script will forward all scroll and scrollend events from the scope of the current page to the service worker’s scope.
...
"content_scripts": [
{
"js": ["scripts/scroll_events_forwarder.js"],
"matches": [
"<all_urls>"
]
}
],
...
The background process running in the browser that is listening to the events coming from the popup and the current page via the content-script. The “type”: “module” parameter allows for use of Ecmascript modules.
...
"background": { "service_worker": "scripts/stats_service_worker.js", "type": "module" },
...
The access to Chrome’s APIs must be explicitly stated. We are using 2 apis beyond the default ‘runtime’ API: ‘storage’ and ’notifications’
"permissions": [
"storage",
"notifications"
]
Testing will require a few additions to your project. They can be added as dev dependencies using npm.
npm install --save-dev <package_name>
Use that command for each the dependencies:
You should end up with the following in your project’s package.json file:
{
"name": "scrolling-zombie",
"version": "1.0.0",
"description": "A chrome extension to help you keep track of your mindless scrolling.",
...
"devDependencies": {
"@babel/preset-env": "^7.24.5",
"babel-jest": "^29.7.0",
"install": "^0.13.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"puppeteer": "^22.10.0"
}
...
}
This expands our project structure to the following:
├── css
│ └── ...
├── fonts
│ └── ...
├── images
│ └── ...
├── popup
│ └── ...
├── scripts
│ └── ...
├── tests
│ └── ...
├── manifest.json
├── jest.config.js
├── babel.config.js
├── mock-extension-apis.js
├── package-lock.json
└── package.json
You can then configure node to run your tests by adding the following scripts section in your package.json file
{
"name": "scrolling-zombie",
"version": "1.0.0",
"description": "A chrome extension to help you keep track of your mindless scrolling.",
...
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test-integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/integration.test.js"
},
...
}
Packaging the chrome extension consist in create a zip archive with the resources required at runtime and uploading it through the developer portal.
#!/bin/bash
rm scrolling-zombie.zip
zip -r scrolling-zombie.zip css fonts images popup scripts LICENSE.txt manifest.json README.md
The Scrolling Zombie archive has the following content:
.
├── LICENSE.txt
├── README.md
├── css
│ └── styles.css
├── fonts
│ └── ZOMBIE.woff
├── images
│ ├── icon128.png
│ ├── icon16.png
│ ├── icon32.png
│ ├── icon48.png
├── manifest.json
├── popup
│ └── stats.html
└── scripts
├── event_messages.js
├── human_readable.js
├── popup.js
├── scroll_events_forwarder.js
├── settings.js
├── stats.js
└── stats_service_worker.js
It is worth noting that Chrome does not guarantee consistent time for tabs that are in the background and may have been put to sleep. I was initially relying on timestamps generated on the page by the content-script but observed that timestamps in the past were being sent under some conditions. I found a few relevant pieces of information, for example this chrome developers article on background tabs
I was initially using the ‘scrollend’ event but found out that it is not always produced when you would expect it. For example, when pressing Page Down and reaching the bottom of the page, the scrollend event is not generated. If using the scrolling wheel on the mouse, it is generated.
The Web Store enforces a few rules when submitting for publication. Some have to do with the validation of the manifest file from a permission standpoint. There’s a requirement that no unused permission be configured. I found it hard to assess this based on the documentation and without attempting to submit.
Remember that the communication between the content-script, popup and service-worker will be broken every time you reload the extension. All pages that were open before the extension reload will need to be reloaded themselves for their injected content-script to reconnect with the extension’s service-worker.
Here’s a link to the extension in the Web Store .
After a few days of using this extension, I was happy to be yanked out of my scrolling daze in a few occasions and I am already thinking about how it could be improved.
for ex: