All work
Extension

Full Page Screenshot

Capture an entire scrolling page as one image — not just the viewport — by scrolling, tiling and stitching, with device-pixel-ratio and sticky-header handling that survives real websites.

What makes it good

  • 1Scroll-and-stitch capture with a measured device pixel ratio, so browser zoom and mixed-DPI monitors do not shift the tiles
  • 2Sticky and fixed elements hidden after the first tile, so a floating header appears once instead of on every screen
  • 3Detects the real scrolling element, so app shells that scroll an inner div do not produce one repeated viewport
  • 4Full page, visible area or dragged region, with a pre-capture size estimate and a cancellable run

How it works

How it works

Chrome only ever hands an extension the visible viewport — captureVisibleTab has no full-page mode. So the page is scrolled in viewport-sized steps and the tiles are stitched back together. The interesting part is everything that has to be true for the seam not to show.

js
for (const y of yStops) {
  await callAgent(tabId, 'setFixedHidden', [index > 0]);
  const actual = await callAgent(tabId, 'scrollTo', [x, y]);
  await delay(SETTLE_MS);

  const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: 'png' });
  await toOffscreen({ type: 'tile', dataUrl, dx: actual.x - region.x, dy: actual.y - region.y });
}

The details that make it work

Device pixel ratio is measured, not trusted. devicePixelRatio disagrees with the real capture under browser zoom and on mixed-DPI multi-monitor setups, so the scale is derived from the first tile that comes back:

js
const scale = bitmap.width / viewportWidth;

Sticky headers are hidden after the first tile, so a floating nav appears once where it belongs instead of being stamped across the whole image.

The real scroller is detected. Plenty of app shells put overflow: hidden on <html> and scroll an inner div; scrolling the document on those pages produces one repeated viewport.

Capture is paced to about two per second, because captureVisibleTab is quota-limited and throws rather than blocking when you exceed it. Tiles overlap by two pixels so a rounded scroll position cannot leave a seam.

Very long pages are downscaled rather than silently returning a blank canvas past Chrome's ~268 megapixel limit — and the viewer says so instead of handing you an empty image.

Why an offscreen document

A service worker has no URL.createObjectURL, so it cannot hand a large image to a tab without base64-ing the entire thing through message passing. Stitching happens in an offscreen document instead, which has a real DOM and can mint a blob URL the viewer reads directly.

js
await chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: ['BLOBS'],
  justification: 'Stitch captured tiles and expose the result as a blob URL.',
});

Tiles are drawn one at a time and released, so peak memory is a single tile plus the destination canvas rather than the whole set.

Using it

Full page, visible area, or a dragged region. The popup measures the page first and tells you what you are about to trigger — 1440 × 8420 px · 7 tiles · ~5s — and a long capture can be cancelled mid-run. The viewer zooms, pans, copies to the clipboard, and saves with a generated filename.