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.
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:
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.
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.