When I started building Weather & Clock Dashboard — a Firefox new tab extension that shows live weather and world clocks — I had a choice: use a modern framework or go vanilla.
I went vanilla. Here's why, and what I learned.
The Case Against Frameworks in Extensions
1. Bundle size matters more in extensions
A React app might add 40KB+ of compressed JS. In a web app that's negligible. In a browser extension, that's loaded on every new tab, blocking render. Users feel it.
My entire extension is ~15KB uncompressed. It renders instantly.
2. No build tooling = no attack surface
Supply chain attacks on npm packages are real. Zero dependencies means:
- No
npm install - No lockfile drift
- No audits needed
- No
node_modulesto accidentally commit
The security model is simple: I wrote all the code, all the code is right here.
3. Extensions live in a privileged context
Browser extensions have access to browser APIs, storage, and (with permissions) cross-origin requests. Introducing a framework that assumes a normal web context can cause subtle issues.
Vanilla JS has direct, obvious access to browser.storage, browser.tabs, etc. No abstraction layer to debug.
What I Actually Used
weather-clock-dashboard/
├── manifest.json
├── newtab.html
├── style.css
└── app.js
That's the entire extension. No src/, no dist/, no build step.
State management
I didn't need Redux. Local state lives in module-scoped variables. Persistent state uses browser.storage.local:
// Save user preferences
async function savePrefs(prefs) {
await browser.storage.local.set({ prefs });
}
// Load on startup
async function loadPrefs() {
const { prefs } = await browser.storage.local.get('prefs');
return prefs || DEFAULTS;
}
Reactivity
Instead of a virtual DOM, I update the DOM directly when data changes:
function updateWeather(data) {
document.getElementById('temp').textContent = `${Math.round(data.temperature)}°`;
document.getElementById('condition').textContent = getConditionText(data.weathercode);
document.getElementById('weather-icon').textContent = getWeatherIcon(data.weathercode);
}
It's fast. No diffing overhead, no reconciliation. Just set the value.
Dark/light mode
CSS custom properties + a data attribute:
:root {
--bg: #ffffff;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--text: #e0e0e0;
}
document.documentElement.setAttribute('data-theme', prefs.theme);
Two lines to switch themes. No CSS-in-JS needed.
Where This Approach Falls Short
I won't pretend vanilla is always the right call:
- Complex UI state (forms with many dependent fields, wizard flows) gets messy fast
- Large teams benefit from component conventions that frameworks enforce
- Accessibility — frameworks often handle ARIA roles better than hand-rolled JS
For a single-page, single-purpose tool like a new tab extension? Vanilla is plenty.
The Result
The extension works, loads instantly, and has zero runtime errors related to framework incompatibilities. If something breaks, I look at ~300 lines of JS I wrote myself.
Try it on Firefox or view the source — MIT licensed.
Have you shipped a browser extension? What stack did you use? I'm curious whether most extension authors reach for frameworks or go vanilla.
Top comments (0)