Every Firefox extension that replaces the new tab uses the same fundamental mechanism: chrome_url_overrides.newtab in manifest.json. What varies is the complexity of what gets loaded.
Here is the architecture I used across the 6 new tab extensions I built.
The basic mechanism
// manifest.json { "chrome_url_overrides": { "newtab": "newtab.html" } } That is the entire extension-side declaration. Firefox will load newtab.html whenever the user opens a new tab.
The HTML file can be as simple as a static page or as complex as a full React application. The browser treats it like any other extension page — it has access to the browser.* WebExtensions API, but no access to the webpage content of previously open tabs.
Storage
All 6 new tab extensions store data in browser.storage.local. This is the extension-equivalent of localStorage — synchronous by default, persisted to disk, isolated per extension.
// Save await browser.storage.local.set({ tasks: taskArray }); // Load const { tasks } = await browser.storage.local.get('tasks'); The benefit over localStorage: it is accessible from background scripts, content scripts, and popup pages in addition to the new tab page. All extension contexts share the same storage namespace.
The limitation: browser.storage.local is device-local. It does not sync across devices. browser.storage.sync syncs data via the user's Firefox account, but has a 100KB limit and slower read latency. For most of these extensions (task lists, habit grids, stock symbols), local storage is the right choice.
Real-time data: stocks and weather
Stock Market Dashboard and Weather Clock fetch external data. Both use the same pattern:
- Fetch data at load time
- Cache in
browser.storage.localwith a timestamp - On subsequent loads, use the cached value if it is less than N minutes old
- Refetch in the background and update the display when fresh data arrives
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getStockData(symbol) { const { stockCache } = await browser.storage.local.get('stockCache'); const cached = stockCache?.[symbol]; if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } const fresh = await fetchFromAPI(symbol); await browser.storage.local.set({ stockCache: { ...stockCache, [symbol]: { data: fresh, timestamp: Date.now() } } }); return fresh; } The rendering approaches
I used vanilla JavaScript for the simpler extensions (Habit Tracker, Quick Calculator, Daily Planner) and a lightweight reactive approach for extensions with more dynamic state (Stock Market, Weather Clock).
Avoiding a framework dependency keeps the bundle size small (under 50KB for most) and the startup time fast. A new tab opens dozens of times per day — anything that adds 200ms of initialization is noticeable.
Why no external dependencies
All 11 extensions have zero npm dependencies in production. No React, no Vue, no utility libraries. The reasons:
- Bundle size: dependencies add weight; a new tab page should load in under 100ms
- Security review: Firefox AMO reviewers check for known-vulnerable npm packages
- Maintenance: zero dependencies means zero dependency upgrade churn
The tradeoff is more verbose JavaScript. For extensions of this size (most are under 300 lines of logic), this is worth it.
I built 6 of these new tab extensions — Habit Tracker, Daily Planner, Stock Market, Weather Clock, Quick Calculator, Speed Dial, and News Widget. All in the series.
