DEV Community

Cover image for Building Tab Timekeeper: A Small Chrome Extension for Making Browsing Time Visible
tapadyuti chatterjee
tapadyuti chatterjee

Posted on

Building Tab Timekeeper: A Small Chrome Extension for Making Browsing Time Visible

I built Tab Timekeeper around a small question that is surprisingly easy to lose track of while browsing:

How long have I been on this website in this tab?

Browsers make it easy to open a page, follow a link, switch tabs, and return much later. What they do not normally show is the time behind that activity. Tab Timekeeper adds that missing bit of context. Click the extension icon and its popup displays a running timer for the active tab, along with the website's favicon.

The extension is intentionally small. There is no framework, backend, account, analytics pipeline, or cross-device synchronization. It is a Manifest V3 extension built with HTML and plain JavaScript, using browser-provided storage and extension APIs.

That small scope made it a useful project for learning how the parts of a Chrome extension cooperate: the manifest, service worker, content script, popup, page context, and permissions.

Tab Timekeeper running on Wikipedia, with the extension popup showing one minute and four seconds

Tab Timekeeper keeps the interaction simple: open the popup and see the elapsed time for the current website in this tab.

The extension is available on the Chrome Web Store, where it has also received Chrome's Featured badge.

TL;DR

Tab Timekeeper is a lightweight Chrome extension that shows how long the current website has been open in the active tab.

It uses a Manifest V3 configuration, a background service worker, a content script, and a popup written in plain HTML and JavaScript. The content script stores a start timestamp and the current origin in the page's sessionStorage. The popup queries the active tab once per second, injects a small function into that page, and reads back the elapsed time and favicon. The background service worker listens for tab activation and completed page loads, although its timestamp is written to localStorage and is separate from the sessionStorage value currently used by the popup.

The project reinforced a few useful lessons: browser storage scope is an architectural decision, extension contexts need an explicit bridge, permissions should stay narrow, and a small utility benefits from keeping its interface and implementation equally focused.

Transparency note: I used AI to help format and polish the wording of this article. The app, architecture, implementation decisions, and experiences described here are my own.

The idea behind Tab Timekeeper

Many productivity tools begin by trying to change behavior. Tab Timekeeper begins one step earlier: it makes behavior visible.

The goal was not to build a full activity tracker or a system that judges which sites are productive. I wanted a quick answer for the page already in front of me. That led to a deliberately short interaction:

  1. Open or browse to a webpage.
  2. Continue using it normally.
  3. Click the extension icon.
  4. See the elapsed time for that website in that tab.

This can be useful during a study session, while researching a topic, or simply when a short break has quietly become a long one.

The distinction between a website and a page matters here. The implementation uses window.location.origin as the identity of a site. Moving between paths on the same origin can therefore remain part of the same session, while moving to another origin starts a new one.

The extension at a glance

The repository has only a few functional files:

manifest.json
background.js
content.js
popup.html
popup.js
images/
Enter fullscreen mode Exit fullscreen mode

Their responsibilities are straightforward:

  • manifest.json declares the extension, permissions, service worker, popup, icons, and content script.
  • content.js initializes per-tab website timing state in sessionStorage.
  • background.js responds to tab lifecycle events and injects a timestamp update.
  • popup.html defines the compact interface.
  • popup.js queries the active tab, obtains the elapsed time and favicon, and refreshes the display.

Conceptually, the active path looks like this:

Page loads
    ↓
content.js records origin + start time in sessionStorage
    ↓
User opens the extension popup
    ↓
popup.js finds the active tab
    ↓
chrome.scripting.executeScript runs a small reader in that page
    ↓
Elapsed seconds + favicon URL return to the popup
    ↓
The popup formats and refreshes the timer every second
Enter fullscreen mode Exit fullscreen mode

There is no application server in this architecture. All of the work happens locally in Chrome.

Manifest V3 as the wiring diagram

manifest.json is the entry point Chrome uses to understand the extension. Tab Timekeeper declares Manifest V3 and registers background.js as its service worker:

"background": {
  "service_worker": "background.js"
}
Enter fullscreen mode Exit fullscreen mode

It also declares the toolbar popup:

"action": {
  "default_popup": "popup.html"
}
Enter fullscreen mode Exit fullscreen mode

The content script runs on all normal web URLs:

"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": ["content.js"]
  }
]
Enter fullscreen mode Exit fullscreen mode

The extension requests activeTab and scripting, plus host access for HTTP and HTTPS pages. Those permissions match the central operation of the app: identify the current tab and execute a small function inside it.

An earlier version declared the Chrome storage permission, but it was removed. The current implementation does not use chrome.storage; it uses the webpage's own Web Storage instead. Removing an unused permission keeps the manifest closer to the actual design and reduces what the extension asks Chrome and the user to trust.

Timing a website with sessionStorage

The content script begins by reading two values from the current page's sessionStorage:

const baseUrl = window.location.origin;
const storedBaseUrl = window.sessionStorage.getItem('baseUrl');

if (storedBaseUrl !== baseUrl) {
  window.sessionStorage.setItem('baseUrl', baseUrl);
  window.sessionStorage.setItem('startTime', Date.now());
}
Enter fullscreen mode Exit fullscreen mode

This is the core of the timer.

sessionStorage is a good fit for the current behavior because its state is associated with a browsing context rather than a permanent extension-wide history. Each tab can maintain its own timing session. The extension stores a timestamp rather than incrementing a counter in the background, so calculating elapsed time is simply:

Math.floor((Date.now() - startTime) / 1000)
Enter fullscreen mode Exit fullscreen mode

That choice avoids needing a long-running one-second timer merely to preserve state. The popup can calculate the current value whenever it needs to render it.

Using the origin as the boundary also gives the feature a clear meaning: the timer represents time on a website in a tab, not necessarily time on one exact URL.

There is a tradeoff. Web Storage belongs to the page's origin, not to the extension. That keeps the solution lightweight, but it also means this is not a centralized browsing-history model. If I wanted reports across days, aggregate totals, or durable extension-owned data, chrome.storage or another extension-owned persistence layer would be a more appropriate next step.

The popup is the live view

The popup is a small HTML document with two visible pieces: a website icon and a timer.

When its DOM is ready, popup.js asks Chrome for the active tab in the current window:

chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  // Read timing data from the active page.
});
Enter fullscreen mode Exit fullscreen mode

For a normal webpage, it then uses chrome.scripting.executeScript to run getElapsedTimeAndFavicon in the tab. That function reads startTime from the page's sessionStorage, calculates the elapsed seconds, finds the page's favicon link, and returns both values.

The result crosses back through the callback returned by executeScript:

timerElement.textContent = formatTime(results[0].result.elapsedTime);
websiteIconElement.src = results[0].result.faviconUrl;
Enter fullscreen mode Exit fullscreen mode

This is the extension's communication mechanism. It does not use chrome.runtime.sendMessage or a long-lived port. For this small request-response flow, the result of script execution is enough.

The popup repeats the query every second while it is open:

setInterval(updateTimer, 1000);
updateTimer();
Enter fullscreen mode Exit fullscreen mode

Calling updateTimer() immediately is a small but important detail. Without it, the popup would initially show its placeholder and wait up to a second before displaying the real value.

The timer formatter converts raw seconds into a compact Xm YYs display. The popup also avoids trying to inject into chrome:// pages, where ordinary extensions cannot run scripts, and falls back to 0m 00s.

What the service worker does

Manifest V3 background logic runs as a service worker rather than as a permanently open background page.

Tab Timekeeper listens for two events:

  • chrome.tabs.onActivated, when the active tab changes.
  • chrome.tabs.onUpdated, when a tab finishes loading.

Both call updateStartTime, which injects a function into the selected tab and writes the current time to window.localStorage.

It also checks chrome.runtime.lastError, which matters because injection can fail on restricted pages or other contexts where Chrome does not permit the operation.

There is an implementation detail here that is easy to miss: the service worker writes localStorage.startTime, while the content script and popup use sessionStorage.startTime. These are separate stores. As the code currently stands, the visible timer is driven by the content-script/session-storage path; the local-storage timestamp written by the service worker is not read by the popup.

That does not need to be hidden in an architecture explanation. Small projects often preserve traces of an earlier design as the behavior evolves. In a future cleanup, I would either remove the unused background timestamp path or make one component the clear owner of timing state. A single source of truth would make tab activation, page loading, and same-site navigation semantics easier to define and test.

Storage scope was the main architectural choice

The most interesting decision in this extension is not the timer formatting. It is where the timestamp lives.

There are several possible models:

  • A popup-local counter would disappear each time the popup closed.
  • An in-memory service-worker counter would be unreliable because Manifest V3 workers can stop when idle.
  • chrome.storage would provide extension-owned persistence and make aggregation easier.
  • Page localStorage would persist by origin beyond a single tab session.
  • Page sessionStorage naturally supports a tab-oriented session.

The current implementation uses the last option for the value users see. It is a compact solution to the stated feature, and it avoids pretending that the extension is a full historical tracker.

The tradeoff is that exact behavior follows browser storage and navigation rules. That is fine for a focused utility, but it is also why the README's broader claim about displaying total time across all tabs is not something I would make for the current code. The implementation reports the active website's time in the current tab; it does not maintain a cross-tab aggregate.

A small UI with one job

The popup is only 200 pixels wide. It uses inline CSS, a centered 24-pixel timer, and a 16-pixel favicon.

A close-up of the Tab Timekeeper popup showing the Wikipedia favicon and an elapsed time of 12 seconds

The popup combines the active website's favicon with a timer that updates once per second.

That limited interface reflects the product decision: opening the extension should answer the question immediately. There are no dashboards, settings, graphs, or controls competing with the result.

Adding the favicon was a useful improvement over showing only text. It provides a quick visual association with the site without requiring the popup to parse and format a hostname.

There is still room to make this path more defensive. Some pages do not declare a link[rel~="icon"], so favicon lookup should ideally handle a missing element and use a fallback. The popup could also surface a clearer unavailable state for protected browser pages instead of displaying a zero that looks like a measurement.

Decisions and tradeoffs that shaped the project

Plain JavaScript instead of a framework

For five small source files, a UI framework and build pipeline would add more structure than value. Plain JavaScript makes the runtime behavior visible and keeps the extension easy to load unpacked during development.

Calculate from a timestamp instead of counting ticks

The extension stores a start time and derives elapsed seconds. This avoids timer drift caused by assuming that a callback will run at an exact interval, and it fits Manifest V3's event-oriented execution model.

Inject a reader instead of maintaining a message bus

The popup needs two values from one active page. Returning them from executeScript keeps communication direct. A runtime message system would become more valuable if content scripts pushed events, multiple extension views consumed the same state, or the service worker coordinated a larger model.

Keep permissions aligned with behavior

The removal of the unused storage permission is a good example of permission discipline. The current extension still needs broad host access because its feature is intended to work across ordinary HTTP and HTTPS sites, but every declared capability should have a specific reason to exist.

Accept the limits of a focused tool

Tab Timekeeper is not a surveillance-style browser analytics product. It does not collect data, require an account, or send browsing activity to a backend. The architecture supports that claim: timing is performed locally in the browser.

From a tiny utility to a Featured extension

The Chrome Web Store listing currently shows Tab Timekeeper as version 1.1 and marks it Featured. It also describes the extension as collecting no user data.

What I learned

A few lessons from Tab Timekeeper have stayed with me.

First, storage scope defines product behavior. Choosing between page storage and extension storage is not just an implementation detail; it determines whether data belongs to a tab, an origin, the browser profile, or a longer-lived history.

Second, extension code runs in multiple contexts. The service worker, popup, content script, and webpage do not share one global JavaScript environment. Data has to cross those boundaries intentionally, whether through injection results, messaging, or shared extension storage.

Third, Manifest V3 rewards event-driven design. A timestamp that can be evaluated on demand is a better foundation than assuming a background process will remain alive and increment a counter forever.

Fourth, permissions are part of the product. They affect user trust and review, not only whether an API call succeeds.

Finally, small scope makes inconsistencies easier to see. The separate local-storage and session-storage paths work differently. Keeping documentation, manifest permissions, and runtime behavior synchronized is part of maintaining even a very small extension.

What I would improve next

The next version could remain lightweight while tightening a few areas:

  • Consolidate timing state so the content script, service worker, and popup share one explicit source of truth.
  • Decide and document exact reset semantics for tab activation, reloads, and navigation within the same origin.
  • Add safe handling for pages without a declared favicon.
  • Display a clear “unavailable on this page” state for protected Chrome URLs.
  • Add automated tests for elapsed-time formatting and the storage/reset rules.
  • Update the README so every advertised feature matches the shipped implementation.
  • Consider chrome.storage only if durable history or cross-tab totals become a real product requirement.

The important part would be preserving the extension's current character: quick, local, and simple.

Closing thoughts

Tab Timekeeper began with a narrow idea: make the time spent on the current website visible.

Building it showed how much architecture can exist inside a tiny browser utility. The manifest defines trust and capabilities. The content script gives each page context. Web Storage determines the lifetime of the timing session. Script injection bridges the popup and active page. The service worker responds to browser events without needing to stay alive permanently.

None of those pieces is large, but their boundaries matter.

You can try Tab Timekeeper on the Chrome Web Store.

If you try it, I would love to hear whether seeing the timer changes the way you use a tab—or which improvement would make it more useful for you.

Top comments (0)