In the previous part of the series, we learned about the structure of building a Chrome extension. We are ready to build and structure our first Chrome Extension with that knowledge. Let’s get started.
Idea
This is the idea of the project
Getting Started
First, we create an empty directory with a manifest.json
. Initially, we want to inject a script to the current page in the tab by using content_scripts
.
{
"manifest_version": 3,
"name": "Link previewer",
"version": "0.0.1",
"description": "It sneak preview the link by mouse hovering and show key facts using OpenAI GPT model.",
"icons": {
"16": "images/icon-16.png",
"32": "images/icon-32.png",
"48": "images/icon-48.png",
"128": "images/icon-128.png"
},
"content_scripts": [{
"js": ["dist/index.js"]
}]
}
Note that I started with a dist/index.js
. This is where we put the bundled code into. For now, let’s create a src/index.ts
. Now, we can add Bun by bun init
. It will ask if you want to use Typescript. Go ahead and use it. Then, we need to adjust the package.json
.
{
// ..
"scripts": {
"dev": "bun build --watch src/index.ts --outfile dist/index.js"
}
// ..
}
We want to plan Typescript, so let’s install it: bun install -D chrome-types
.
Now, we need to add the types to the tsconfig.json
{
// tsconfig.json
// ..
"types": ["chrome-types"]
}
We need that chrome
- object. Otherwise, our IDE may yell at me.
If you enter their code from their Chrome Extension Getting Started page, Typescript reveals so many errors🫢
Developing
Okay, it’s time to get our hands dirty. Before that, we need to load an unpacked extension:
- Open
chrome://extensions
in a new tab - Enable the
Developer Mode
and click on theLoad unpacked
- Now, go to the directory of your extension and click on the reload button
You need to reload the extension every time you change, which is quite annoying. Extension Reloader is another extension that can do that for you.
Start Bun
Do you remember the scripts
in the package.json
from above? First, you get Hot Module reloading whenever a change happens on that file. Second, Bun is bundling the index.ts
to index.js
and put it into the dist
- folder.
bun dev
Add a listener on each link
From our idea, we want to show a tooltip whenever we hover over a link on that page. In doing that, we have to add a listener.
const links = document.querySelectorAll('a')
console.log(links) // Debugging purpose
Go ahead and console.log
it and check the links. The querySelectorAll
returns a list of Anchor elements.
But here is a little problem. We get a NodeList
that is not iterable, and what if an Anchor tag does not href
, which we need in our case? So, let’s modify it a bit
const links = Array.from(document.querySelectorAll("a")).filter((link) =>
link.hasAttribute("href"),
); // Create an Array from an iterable object
// to verify
links.forEach((link) => {
console.log(link.href);
});
We can add an event listener whenever the mouse enters the link.
const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));
links.forEach(link => {
link.addEventListener('mouseenter', () => {
console.log(link.href);
});
});
Each link gets an event listener mouseenter
whenever the mouse is entering or hovering over the link.
But what if the page has many links? On Arc, you can hold shift on a link, but it seems to be impossible with vanilla JavaScript. Let’s extend it and add a timer when the user has been on the link for 3 seconds.
const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));
links.forEach(link => {
link.addEventListener('mouseenter', () => {
console.log(link.href);
const timer = setTimeout(function() {
// Here we put the logic after 3000 ms aka 3 seconds
}, 3000); // Adjust the time (in milliseconds) as needed
link.addEventListener("mouseleave", function() {
// Clear the timer if the mouse leaves the link before the specified time
clearTimeout(timer);
});
});
});
We execute the logic after 3 seconds via setTimeout
and add another event listener when the user leaves the mouse after leaving the link.
Show the Tooltip
Now, we want to add the tooltip. The approach is that we create a tooltip element, but where to add it? What happens if we hover on the same link again?
The idea is that we create the tooltip and put the element at the bottom of the <body>
.
Here, we outsource the code logic in tooltip.ts
, to make it more readable. In addition, we create some listeners on the link we’ve just hovered over.
// in tooltip.ts
export function createTooltip(link: HTMLAnchorElement) {
const tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.dataset.url = link.href;
tooltip.innerHTML = 'This is a tooltip';
document.body.appendChild(tooltip);
createTooltipAction(link, tooltip);
return tooltip;
}
function createTooltipAction(link: HTMLAnchorElement, tooltip: HTMLElement) {
function onMouseEnter() {
console.log('onMouseEnter');
}
function onMouseMove(event: MouseEvent) {
console.log('onMouseMove');
tooltip.style.backgroundColor = 'white';
tooltip.style.color = 'black';
tooltip.style.display = 'block';
tooltip.style.position = 'absolute';
tooltip.style.top = `${event.pageY - 25}px`;
tooltip.style.left = `${event.pageX - 10}px`;
}
function onMouseLeave() {
console.log('onMouseLeave');
tooltip.style.display = 'none';
}
link.addEventListener('mouseenter', onMouseEnter);
link.addEventListener('mousemove', onMouseMove);
link.addEventListener('mouseleave', onMouseLeave);
return {
destroy() {
link.removeEventListener('mouseenter', onMouseEnter);
link.removeEventListener('mousemove', onMouseMove);
link.removeEventListener('mouseleave', onMouseLeave);
},
};
}
Notice that the tooltip contains the attribute data-url={link.href}
. That helps us to find the tooltip and avoid recreating it.
// in index.ts
import { createTooltip } from './tooltip';
const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));
links.forEach(link => {
link.addEventListener('mouseenter', () => {
const tooltip = document.querySelector<HTMLElement>(
`[data-url="${link.href}"]`,
);
if (tooltip === null) {
createTooltip(link);
}
});
});
Let’s add a nice skeleton animation when the tooltip appears
// tooltip.ts
export function createTooltip(link: HTMLAnchorElement) {
// ...
createTooltipSkeleton();
return tooltip;
}
function createTooltipSkeleton() {
let linkElement = document.createElement("style");
linkElement.innerHTML = `
.tooltip {
width: 220px;
height: 80px;
border-radius: 5px;
}
.tooltip .avatar {
float: left;
width: 52px;
height: 52px;
background-color: #ccc;
border-radius: 25%;
margin: 8px;
background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
background-size: 600px;
animation: shine-avatar 1.6s infinite linear;
}
.tooltip .line {
float: left;
width: 140px;
height: 16px;
margin-top: 12px;
border-radius: 7px;
background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
background-size: 600px;
animation: shine-lines 1.6s infinite linear;
}
.tooltip .avatar + .line {
margin-top: 11px;
width: 100px;
}
.tooltip .line ~ .line {
background-color: #ddd;
}
@keyframes shine-lines {
0% {
background-position: -100px;
}
40%, 100% {
background-position: 140px;
}
}
@keyframes shine-avatar {
0% {
background-position: -32px;
}
40%, 100% {
background-position: 208px;
}
}
`;
// Append the link element to the head of the document
document.getElementsByTagName("head")[0].appendChild(linkElement);
}
At this point, we can ask if a framework like React or Vue is not a better choice for better maintenance. But since we use Bun, it is supposed to bundle .tsx
files out of the box. That is unfortutately not the case as you need a third party library such as React or Preact. I chose the latter: bun add -D preact
and adjust the tsconfig.json
:
{
"compilerOptions": {
// ...
/* Preact */
"jsx": "react-jsx",
"jsxImportSource": "preact",
"baseUrl": "./",
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
}
}
}
Now, we can rewrite the Tooltip.tsx
:
import * as React from "preact";
export default function Tooltip(props: { link: HTMLAnchorElement }) {
console.log("Tooltip", props.link.href);
const tooltip = React.createRef();
createTooltipSkeleton();
function onMouseMove(event: MouseEvent) {
console.log("onMouseMove");
tooltip.current.style.backgroundColor = "white";
tooltip.current.style.color = "black";
tooltip.current.style.display = "block";
tooltip.current.style.position = "absolute";
tooltip.current.style.top = `${event.pageY - 100}px`;
tooltip.current.style.left = `${event.pageX - 100}px`;
}
function onMouseLeave() {
console.log("onMouseLeave");
tooltip.current.style.display = "none";
}
props.link.addEventListener("mousemove", onMouseMove);
props.link.addEventListener("mouseleave", onMouseLeave);
return (
<div className="tooltip" data-url={props.link.href} ref={tooltip}>
<div className="avatar"></div>
<div className="line"></div>
<div className="line"></div>
</div>
);
}
The compiler complains if we don’t import React
somewhere as if JSX is only possible with React… The VS Code compiler also put React in favor when using JSX
I honestly don’t like that approach. There may be good alternatives, such as VanJS, which does not use JSX, and nanoJSX. VanJS results in a smaller bundle ( < 8kb) **but is not so nicely readable as Preact ( < 19kb**).
Anyway, I will put both codes in the repository.
The result is the same 🤩
Conclusion
The blog post discusses building a basic Chrome extension that displays a tooltip with additional information when the user hovers their mouse over web links. It covers setting up the project structure, adding Typescript for type safety, using Bun to bundle the code, selecting links and adding mouseenter
listeners, conditionally displaying a tooltip after 3 seconds, creating the tooltip element and styling it, and positioning the tooltip near the mouse pointer. The post also explores using Preact to manage the tooltip component and compares it to other frameworks like VanJS and NanoJSX that could potentially reduce the bundle size. Overall, the post provides a helpful tutorial for creating a foundational Chrome extension.
Find the repository here.
Check out this repository if you decide to use another framework for your Chrome extension: https://github.com/guocaoyi/create-chrome-ext.
In the next section, we will integrate OpenAI’s SDK and explore the capabilities of Langchain.
Top comments (0)