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:

  1. Fetch data at load time
  2. Cache in browser.storage.local with a timestamp
  3. On subsequent loads, use the cached value if it is less than N minutes old
  4. 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:

  1. Bundle size: dependencies add weight; a new tab page should load in under 100ms
  2. Security review: Firefox AMO reviewers check for known-vulnerable npm packages
  3. 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.