This tutorial will guide you through building a Chrome extension that displays the user's weather details in the bottom-right corner of every website. Key benefits include convenient weather information, personalised experience, improved productivity, and seamless Chrome integration.
You will learn how to:
Render a custom React component on any website
Bundle React code using Webpack
Fetch data in Chrome extensions
Set up the Chrome extension configuration
Communicate between content scripts and the extension
After completing this tutorial, you will gain skills in Chrome extension development, including accessing user location, fetching weather data, and designing a custom user interface. The result will be a functional extension
Before you start
This guide assumes that you have basic React development experience. I recommend checking out React Docs and Chrome Extension Docs and getting an API key from weatherapi
Build the extension
To start, create a new weather
directory to hold the extension's files.
Add the extension data and icons
Create a file called manifest.json
and include the following code;
{
"manifest_version": 3,
"name": "Weather Widget",
"description": "Extension that shows the day weather and forecast .",
"version": "1.0",
"icons": {
"16": "images/weather-46-16.png",
"32": "images/weather-46-32.png",
"48": "images/weather-46-48.png",
"128": "images/weather-46-128.png"
},
"content_scripts": [
{
"js": [
"contentScript.js"
],
"matches": [
"<all_urls>"
]
}
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Weather",
"default_popup": "popup.html"
},
"author": "Tobiloba Adebisi https://www.github.com/Adebisi1234",
"host_permissions": [
"<all_urls>"
],
"permissions": [
"storage",
"geolocation"
],
"web_accessible_resources": [
{
"resources": [
"images/refresh-34-16.png"
],
"matches": [
"<all_urls>"
]
}
]
}
Create an images
folder then download the icons
Add dependencies
Configure your package.json
file like this;
{
"name": "weather",
"version": "1.0.0",
"description": "",
"main": "main.js",
"type": "module",
"scripts": {
"build": "webpack --config webpack.config.cjs",
"watch": "webpack -w --config webpack.config.cjs",
"types": "npx tsc"
},
"keywords": [],
"author": "Tobiloba Adebisi https://github.com/adebisi1234",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.25.0",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@types/chrome": "^0.0.269",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"html-loader": "^5.1.0",
"html-webpack-plugin": "^5.6.0",
"style-loader": "^4.0.0",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}
And then install the dependencies using npm install
Setup Webpack
Create a webpack.config.cjs
file and include the following code.
const path = require("path");
const HTMLPlugin = require("html-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
entry: {
contentScript: "./src/content/index.tsx",
background: "./src/background/index.tsx",
installed: "./src/installed/index.tsx",
popup: "./src/popup/index.tsx",
},
mode: "production",
target: "web",
module: {
rules: [
{
test: /\.tsx$/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
},
},
exclude: /node_modules/,
},
{
test: /\.html$/,
loader: "html-loader",
exclude: /node_modules/,
},
{
exclude: /node_modules/,
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HTMLPlugin({
template: path.join(__dirname, "src", "popup", "index.html"),
filename: "popup.html",
chunks: ["popup"],
cache: false,
}),
new HTMLPlugin({
template: path.join(__dirname, "src", "installed", "index.html"),
filename: "installed.html",
chunks: ["installed"],
cache: false,
}),
new CopyPlugin({
patterns: [
{ from: path.resolve("manifest.json"), to: path.resolve("dist") },
{ from: path.resolve("images"), to: path.resolve("dist/images") },
],
}),
],
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
clean: true,
},
};
This defines how Webpack will bundle our React code, it'll emit the following files in the dist
folder
/images
- Folder containing all images used in the extension.background.js - Service worker used to fetch weather data.
manifest.json - Configuration for the extension.
installed.html - The webpage is shown when the extension gets installed
popup.html - The webpage shows when the extension gets clicked
contentScript.js - React component that gets injected into every webpage
installed.js - React component for the installed.html page
popup.js - React component for the popup page
File Structure
Create the following files and folder in the src
folder
Installed page
This page is used to collect the user's location details that will be used for fetching the weather data.
Include the following code in /src/installed/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to weather extension</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
A simple HTML page that defines the root element <div id="root"></div>
in which your React component gets rendered.
To attach your React component for the installed page to the root element, include the following code in `/src/installed/index.tsx.
import React from "react";
import { createRoot } from "react-dom/client";
import Installed from "./Installed";
const root = createRoot(document.getElementById("root") as HTMLElement)
root.render(<Installed />);
In /src/installed/Installed.tsx
include the following code
import React, { useEffect, useRef, useState } from 'react'
export default function Installed() {
const inputRef = useRef<HTMLInputElement>(null)
const [useGeoLocation, setUseGeoLocation] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!useGeoLocation) return;
navigator.geolocation.getCurrentPosition(async (position) => {
await chrome.storage.local.set({ location: { type: "name", value: `${position.coords.latitude},${position.coords.longitude}` } })
window.close()
}, (error) => {
alert("Error getting geolocation" + error.message)
})
}, [useGeoLocation])
return (
<div className='container'>
<h4>Welcome to the weather widget</h4>
<p>Please enter your location, to get accurate forecasts</p>
<form onSubmit={(e) => {
e.preventDefault()
setLoading(true)
}}>
<fieldset>
<input type="text" placeholder='city name' ref={inputRef} />
<button type='submit' onClick={async () => {
if (!inputRef.current || inputRef.current.value.trim() === "") return;
await chrome.storage.local.set({ location: { type: "name", value: inputRef.current.value } })
window.close()
}}>Submit</button>
</fieldset>
<button type='submit' onClick={() => {
setUseGeoLocation(true)
}} >Use Geolocation</button>
</form>
{loading && <p>Setting up...</p>}
</div>
)
}
This gets the user's location either by using the geolocation API or the user's manually inputted location and then saves it using the Storage API.
Service worker
Here, we define the background service, and these are its functions:
Open the Installed page when the extension gets installed
in
/src/background/index.tsx
include the following codechrome.runtime.onInstalled.addListener(function (object) { const internalUrl = chrome.runtime.getURL("installed.html"); if (object.reason === chrome.runtime.OnInstalledReason.INSTALL) { chrome.tabs.create({ url: internalUrl }, function (tab) { console.log("New tab launched with installed.html"); }); } });
Fetch weather data - The content script can't make API calls, so the task will fall on the background service. In the
index.tsx
file, include the following code:export const BASE_URL = "https://api.weatherapi.com/v1"; export const API_KEY = /*<API_KEY>*/; chrome.runtime.onMessage.addListener(async function (msg) { if (msg.type === "weather") { const activeId = await chrome.tabs.query({ active: true, currentWindow: true }) const response = await getWeather() chrome.tabs.sendMessage(activeId[0]?.id ?? 0, response) } }) async function getWeather() { const cache = await chrome.storage.local.get("weather") // Same Day if (cache?.weather?.date === new Date().toDateString() && cache.weather.data) { return { message: "success", data: cache.weather.data } } else { const loc = await chrome.storage.local.get("location") console.log(loc.location) const data = await fetch(`${BASE_URL}/current.json?key=${API_KEY}&q=${loc.location.value}`) const res = await data.json() console.log(res) if (data.ok) { chrome.storage.local.set({ "weather": { data: res, date: new Date().toDateString() } }) return { message: "success", data: res } } else { return { message: "failed", data: res } } } }
This caches the weather data gotten from the weather API, only gets new results once daily, and returns the data to the content script.
Content Script
First, you create a DOM element which your React component can get attached to on the website
In your /src/content/index.tsx
file, include the following code:
import React from "react";
import { createRoot } from "react-dom/client";
import Widget from "./Widget";
import "./styles.css";
setTimeout(() => renderWidget());
function renderWidget() {
const rootEl = document.createElement("div");
rootEl.id = "widget_root";
document.body.append(rootEl);
console.log(document.getElementById("widget_root"));
const root = createRoot(document.getElementById("widget_root")!);
root.render(<Widget />);
}
The setTimeout
ensures the renderWidget
function gets called only after the page has been loaded. Next, you create the React component that shows the weather details
In the /src/content/Widget.tsx
file, include the following code:
import React, { useEffect, useState } from "react";
import "./styles.css";
export interface Weather {
location: {
name: string;
region: string;
country: string;
lat: number;
lon: number;
tz_id: string;
};
current: {
last_updated: string;
temp_c: string;
temp_f: string;
condition: {
text: string;
icon: string;
code: number;
};
};
}
export default function Widget() {
const [useCel, setUseCel] = useState(true);
const [weather, setWeather] = useState<Weather | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
chrome.runtime.sendMessage({ type: "weather" });
chrome.runtime.onMessage.addListener((response) => {
console.log(response);
if (response.message === "success") {
setWeather(response.data);
setLoading(false);
}
});
})();
}, []);
return (
<>
{loading ? (
<div>loading...</div>
) : (
weather && (
<div>
<span className="badge">{weather?.location.name}</span>
<div className="container">
<div className="details">
<img
src={`https://${weather?.current.condition.icon.substring(
2
)}`}
alt={weather?.current.condition.text}
width={16}
height={16}
/>
<button onClick={() => setUseCel((prev) => !prev)}>
<p>
{useCel
? `${weather?.current.temp_c}°C`
: `${weather?.current.temp_f}°F`}
</p>
</button>
</div>
<div
className="reload"
onClick={() => chrome.runtime.sendMessage({ type: "weather" })}
>
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M904.161882 587.294118c-13.733647 0-25.238588 9.456941-28.973176 22.708706C832.451765 761.976471 693.007059 873.411765 527.36 873.411765c-165.586824 0-305.091765-111.435294-347.828706-263.408941-3.704471-13.251765-15.239529-22.708706-28.973176-22.708706-19.968 0-34.304 19.184941-28.912942 38.4C171.188706 803.297882 333.914353 933.647059 527.299765 933.647059c193.475765 0 356.141176-130.349176 405.775059-307.952941a30.177882 30.177882 0 0 0-28.912942-38.4M150.528 436.705882c13.733647 0 25.268706-9.487059 28.973176-22.708706C222.238118 262.023529 361.743059 150.588235 527.36 150.588235c165.616941 0 305.121882 111.435294 347.858824 263.408941 3.704471 13.221647 15.209412 22.708706 28.943058 22.708706 19.998118 0 34.304-19.184941 28.912942-38.4C883.471059 220.702118 720.805647 90.352941 527.36 90.352941S171.218824 220.702118 121.615059 398.305882a30.177882 30.177882 0 0 0 28.912941 38.4"
fill="#fff"
/>
<path
d="M86.949647 369.392941l138.691765 33.822118a12.047059 12.047059 0 0 1 5.180235 20.690823l-100.201412 89.840942a12.047059 12.047059 0 0 1-19.546353-5.421177L72.583529 384.692706a12.047059 12.047059 0 0 1 14.366118-15.269647zM972.077176 646.896941l-141.131294-21.62447a12.047059 12.047059 0 0 1-6.927058-20.148706l91.949176-98.213647a12.047059 12.047059 0 0 1 19.937882 3.644235l49.121883 119.868235a12.047059 12.047059 0 0 1-12.950589 16.474353z"
fill="#fff"
/>
</svg>
</div>
</div>
</div>
)
)}
</>
);
}
Since content scripts can't make API calls, we send a message to the service worker for the weather details, then listen for its response, and show the data if the response was successful.
then, the styling for the widget
Include the following in /src/content/styles.css
#widget_root {
width: fit-content;
height: fit-content;
position: fixed;
left: 99%;
top: 98%;
transform: translateX(-100%) translateY(-100%);
color: white;
padding: 10px;
border-radius: 8px;
/* From https://css.glass */
background: rgba(102, 51, 153, 0.08);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3.9px);
-webkit-backdrop-filter: blur(3.9px);
border: 1px solid rgba(102, 51, 153, 0.3);
}
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.details {
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
}
.reload {
width: 16px;
height: 16px;
display: flex;
align-items: center;
}
.badge {
border-radius: 4px;
font-size: 12px;
background-color: rgb(52, 52, 52);
color: aliceblue;
padding: 2.5px;
}
svg {
width: 16px;
height: 16px;
}
Popup page
For this page, you'll be reusing the Widget component (that's the fun of React)
Create a DOM element your React code gets attached to
In
/src/popup/index.html
include the following code<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style> html { min-height: 5em; min-width: 10em; /* From https://css.glass */ background: rgba(102, 51, 153, 0.3); border-radius: 16px; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); backdrop-filter: blur(3.9px); -webkit-backdrop-filter: blur(3.9px); border: 1px solid rgba(102, 51, 153, 0.3); color: black; } </style> <title>Weather Widget</title> </head> <body> <div id="root"></div> </body> </html>
Couple your react component to the HTML
in
/src/popup/index.tsx
add the following codeimport React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; const root = createRoot(document.getElementById("root") as HTMLElement) root.render(<App />);
Since the popup page can make API calls, we'll use it to fetch the weather data and reuse the Widget component. Include the following code in your
/src/popup/App.tsx
file:import React, { useEffect, useState } from 'react' import { API_KEY, BASE_URL } from '../background' import Widget, { Weather } from '../content/Widget' export default function App() { const [data, setData] = useState<Weather | null>(null) useEffect(() => { (async () => setData((await getWeather()).data))() }, []) return ( <>{data ? <Widget data={data} /> : <p>Loading...</p>}</> ) } async function getWeather() { const cache = await chrome.storage.local.get("weather") // Same Day if (cache?.weather?.date === new Date().toDateString() && cache.weather.data) { return { message: "success", data: cache.weather.data } } else { const loc = await chrome.storage.local.get("location") console.log(loc.location) const data = await fetch(`${BASE_URL}/current.json?key=${API_KEY}&q=${loc.location.value}`) const res = await data.json() console.log(res) if (data.ok) { chrome.storage.local.set({ "weather": { data: res, date: new Date().toDateString() } }) return { message: "success", data: res } } else { return { message: "failed", data: res } } } }
To make this work, we'll need to edit our widget component a bit. Add the following code to your
/src/content/Widget.tsx
file:
...
export default function Widget({ data }: { data?: Weather }) {
const [useCel, setUseCel] = useState(true);
if (data) {
return (
<div>
<span className="badge">{data?.location.name}</span>
<div className="container">
<div className="details" title={data?.current.condition.text}>
<img
src={`https://${data?.current.condition.icon.substring(2)}`}
alt={data?.current.condition.text}
width={16}
height={16}
/>
<button
onClick={() => setUseCel((prev) => !prev)}
title={data?.current.condition.text}
>
<p>
{useCel
? `${data?.current.temp_c}°C`
: `${data?.current.temp_f}°F`}
</p>
</button>
</div>
</div>
</div>
);
}
const [weather, setWeather] = useState<Weather | null>(null);
...
Finally, compile your code using the npm run build
command and you'll have your bundled Chrome extension under the dist
directory
Load your extension locally
To load an unpacked extension in developer mode, follow the steps in the docs.
After all this, you should have an extension that looks like the one in the image below.
With the skills you've acquired, you can potentially extend this project to include additional features, such as weather alerts, customisable widget settings, or integration with other web services. The possibilities are endless for building Chrome extensions that improve your daily workflow and provide valuable information at your fingertips. For further reading on Chrome extension development and related topics, you might consider Chrome Extensions Documentation
Thanks for reading. If you prefer, you can download the complete source code from GitHub.
Please follow me here and below: