DEV Community

Cover image for I Built CaptureKit: A Privacy-First Browser Capture Extension
Hamza
Hamza

Posted on

I Built CaptureKit: A Privacy-First Browser Capture Extension

I recently built CaptureKit, a browser extension for capturing web content without sending it to a remote server.

The idea was simple: I wanted one lightweight tool for the three capture actions I use most often:

  • Full-page screenshots
  • Selected-area screenshots
  • Short tab recordings

Why build another capture extension?

There are already many screenshot tools available, but I wanted CaptureKit to follow a local-first approach.

Captured content is processed locally in the browser. There is no account requirement, no cloud upload for captures, no analytics, and no advertising.

Short recordings are temporarily stored in the browser and automatically expire after 5 minutes.

The technical side

CaptureKit is built with:

  • WXT
  • React
  • TypeScript
  • IndexedDB
  • Browser Extension APIs
  • MediaRecorder

One interesting challenge was supporting multiple browsers.

Chromium-based browsers can use tabCapture for tab recording, while Firefox requires a different approach using its native display-media selection flow.

Full-page screenshots also required handling scrolling, viewport segmentation, device pixel ratio, image stitching, and restoring the page correctly after capture.

What CaptureKit can do

Full Page

Capture an entire webpage, including content outside the visible viewport.

Selected Area

Draw a selection directly on the page and capture only that region.

Record Tab

Create a short recording of a browser tab, with a maximum duration of 60 seconds.


Captures can then be previewed, copied, downloaded, or deleted locally.

Building for Chrome, Edge, and Firefox

One of my goals was to avoid maintaining three completely separate extensions.

WXT made it possible to share most of the codebase while still handling browser-specific behavior where necessary.

The project currently targets:

  • Chrome
  • Microsoft Edge
  • Firefox

I'm now going through the store publication process and testing the extension across the three browsers.

What's next?

The current focus is reliability: testing different websites, zoom levels, high-DPI displays, long pages, recording permissions, and browser-specific edge cases.

This project has also been a useful exercise in understanding how modern browser extension APIs differ between Chromium and Firefox.

If you work with browser extensions, I'd be interested to hear about the cross-browser issues you've encountered.

Top comments (2)

Collapse
 
raknaos profile image
Baptiste Le Bouquin

The local-first constraint is a good forcing function — it pushes you into the annoying browser APIs instead of letting a server paper over them. The tabCapture vs Firefox split is the part I know best from maintaining a Chromium extension: the API surface silently differs per browser in ways you only discover in production, so WXT sharing code across all three targets sounds like the right call.

How are you handling codecs for the tab recordings? MediaRecorder defaults have bitten us before — Chrome happily does vp8/vp9 in webm, but the supported-types matrix gets ugly the moment you want predictable bitrates or broader playback compatibility. And the 5-minute expiry: TTL sweep on startup, periodic alarm, or do expired recordings just get ignored in place? We hit the same question with cached blobs in IndexedDB and never found an answer we loved.

One suggestion from scar tissue: full-page capture on pages with position:fixed headers or lazy-loaded images is where stitching implementations fall apart — sticky elements repeating across every viewport segment, images that only fetch when scrolled into view. If you've got a story for handling those two, that's the demo that sells the extension.

Collapse
 
hamzatopo profile image
Hamza

Thanks — this is exactly the kind of feedback I was hoping to get from people who have dealt with these APIs in production.
On codecs, I’m trying not to trust the MediaRecorder default blindly. On Chromium I probe with MediaRecorder.isTypeSupported() and prefer WebM/VP8, with fallbacks depending on what the browser exposes. I’m deliberately avoiding promising a perfectly identical codec/bitrate combination across browsers for now, because Firefox and Chromium diverge pretty quickly there. The goal is predictable local playback first; broader export compatibility can come later.
The 5-minute TTL is metadata stored alongside the Blob in IndexedDB rather than relying on the browser to expire anything for me. Cleanup is intentionally best-effort: expired captures are filtered out when the local store is initialized/read, and I’m keeping the cleanup mechanism lightweight rather than running a permanent background worker just for TTL. I may add an alarm-based sweep if storage behaviour in real usage justifies it.
And yes — position: fixed, sticky headers and lazy loading are probably the nastiest part of full-page capture. Lazy-loaded content is easier: the capture pipeline scrolls through the page rather than jumping straight to a final bitmap, giving viewport-dependent content a chance to load before each segment is captured.
Fixed/sticky elements are the harder case. I don’t want to claim that part is completely solved yet. The direction I’m taking is to identify viewport-attached elements during the capture session, temporarily neutralize/reposition them for intermediate segments, then restore the page state afterwards. There are plenty of edge cases once you add nested scroll containers and sites doing their own scroll-driven rendering.
I agree with your last point though: a page with a sticky navbar + several lazy-loaded images is probably a much better real-world demo than capturing a clean documentation page. That’s going straight into my cross-browser test set