Screenshots

11 min read

Why Your Full-Page Screenshot Is Missing Content

Blank images, a header repeated down the page, a capture that stops halfway. Five causes — lazy loading, sticky elements, canvas limits, animation, virtualised lists — and the fix for each.

A tall screenshot in four bands: normal content, a blank band, a repeated navigation bar, and a band cut off by a red line.
Three separate failures in one capture: a blank region, a duplicated header, and an image that simply stops. They have nothing to do with each other.

A full-page screenshot that comes back with holes in it feels like a broken tool. It usually isn’t. Each way a capture fails maps to a specific thing the page is doing — and once you can name which one you are looking at, four of the five fixes take about ten seconds.

Start with the symptom

Every one of these has a different cause and a different fix, so it is worth spending ten seconds identifying which you have before changing anything.

Diagnostic table mapping five screenshot symptoms to their causes and fixes: blank images to lazy loading, header repeats to sticky elements, stops halfway to the canvas ceiling, blurred motion to animation, and empty list to virtualised list.
The whole article in one table. If you only need the answer, it is here; the sections below explain why each one happens.

Blank images: lazy loading

The single most common failure. You get a full-height capture with the right layout, correct text, correct spacing — and every image below the first screen is an empty box.

Two page outlines: the left captured too fast with three empty crossed image placeholders, the right scrolled first with the same three slots filled.
The only difference between these two captures is whether anything scrolled before the shutter.

Why it happens

Modern pages do not download images they cannot see. Either the browser does it natively via loading="lazy", or the site uses an IntersectionObserver that swaps a placeholder for the real src when an element approaches the viewport. Both are triggered by the same thing: the element getting close to the visible area.

A capture method that resizes the viewport to the document height — which is what Chrome DevTools’ Capture full size screenshot does — never scrolls. Nothing ever approaches the viewport, so nothing ever loads, and the shutter fires on a page full of placeholders.

The fix

  1. Press End to jump to the bottom of the page, or hold Page Down until you get there.
  2. Wait for the images to appear. On a long page this can take several seconds.
  3. Press Home to return to the top.
  4. Now capture.

Do it slowly. Jumping straight to the bottom can skip elements in the middle whose observers never fire because they were never near the viewport for a frame.

Automating it? Scroll in steps rather than jumping, and give the page a beat to catch up:

Playwright — scroll the page before capturing
await page.evaluate(async () => {
  const step = window.innerHeight;
  for (let y = 0; y < document.body.scrollHeight; y += step) {
    window.scrollTo(0, y);
    await new Promise((r) => setTimeout(r, 150));
  }
  window.scrollTo(0, 0);
});
await page.waitForTimeout(500);
await page.screenshot({ path: 'page.png', fullPage: true });

A header on every screen: sticky elements

You get the whole page, but the navigation bar appears four times down the image, each time stamped over content it is hiding.

Three viewport passes each containing the same navigation bar, stitched into one tall image where that bar appears three times, circled in red.
A sticky header stays pinned to the viewport, so it is genuinely in every photograph. The stitcher is not making a mistake.

Why it happens

An element with position: fixed or position: sticky is anchored to the viewport rather than the document. During a scroll-and-stitch capture the viewport moves down the page, so the header dutifully comes along, and it is honestly present in every tile. The same applies to cookie banners, chat bubbles, back-to-top buttons and sticky table headers.

The fix

The element has to be hidden for the duration of the capture, or made to scroll with the page. Good extensions do this automatically — Grabby freezes fixed and sticky elements before the passes begin and restores them afterwards, so they appear exactly once, at the top, where they were.

If you are working around a tool that does not, run this in the console first:

Console — pin fixed elements to the document
// Neutralise fixed/sticky positioning for the length of the capture.
const pinned = [...document.querySelectorAll('*')].filter((el) => {
  const p = getComputedStyle(el).position;
  return p === 'fixed' || p === 'sticky';
});
pinned.forEach((el) => { el.dataset.gPos = el.style.position; el.style.position = 'absolute'; });

// After capturing, put them back:
// pinned.forEach((el) => { el.style.position = el.dataset.gPos || ''; });

Setting them to absolute rather than hiding them keeps the header visible once, at the top of the capture, which is usually what you want. Set display: none instead if you want it gone entirely — but be aware that removing a fixed header can shift the layout of pages that compensate for its height.

Stops halfway: the canvas ceiling

The capture is fine, and then it just ends — sometimes mid-sentence, sometimes with a band of solid colour running to the bottom.

A tall document with dashed limit lines at 16,384px and 32,767px, the region above the upper line hatched out, and the same document redrawn as five separate tiles.
Two different ceilings. The lower one limits how tall a single render can be; the upper one limits how tall the finished image can be.

Why it happens

There are two separate limits, and they bite at different stages.

  • The render surface is capped by the compositor’s maximum texture size, commonly 16,384 pixels. Any method that renders the entire document in one pass — DevTools’ full-size capture, fullPage: true in Puppeteer — hits this and truncates. It is discussed at length in Puppeteer issue #359.
  • The 2D canvas that a stitching tool composites into is capped at 32,767 pixels on a side in Chrome, with a further ceiling on total area. A tool that hits this has to downscale the result or refuse.

Both are in device pixels, not CSS pixels. On a HiDPI display where the device pixel ratio is 2, a 9,000px-tall page is already an 18,000px-tall image — past the first limit before you have scrolled anything.

The fix

  • Use a scroll-and-stitch tool. Each photograph is only viewport-sized, so the render limit never applies. Only the final composite has to fit in a canvas, and that limit is twice as high.
  • Capture in sections. Two or three captures of a genuinely enormous page are more useful than one that got truncated, and you can hand them over as a set.
  • Accept a downscale. A tool that scales a 40,000px capture down to fit keeps every part of the page at slightly lower resolution — usually a better trade than losing the bottom third. Grabby does this rather than failing, so a very long page comes back complete.

Smears and mismatches: animation

Elements appear half-faded, a carousel shows two slides at once, or a scroll-triggered section is caught mid-transition with its text at 40% opacity.

Scroll-and-stitch captures take their photographs over one to sixty seconds. Anything moving during that window is in a different position in each tile. Scroll-triggered reveal animations are the worst offenders, because the act of capturing is what triggers them — and the shutter often fires before the transition finishes.

The fix

Disable animation for the duration. If your operating system has a “reduce motion” accessibility setting, turning it on makes well-built sites skip their transitions entirely. Otherwise, force it from the console:

Console — stop everything moving
const style = document.createElement('style');
style.textContent = `*, *::before, *::after {
  animation-duration: 0s !important;
  animation-delay: 0s !important;
  transition-duration: 0s !important;
  transition-delay: 0s !important;
  scroll-behavior: auto !important;
}`;
document.head.appendChild(style);

Setting durations to zero rather than animation: none matters: it snaps every animation to its end state, so reveal-on-scroll sections land fully visible instead of never appearing at all. A tool that freezes the page before capturing does the same thing for you.

Missing rows: virtualised lists

You capture a table of 5,000 rows and get twenty. Scrolling manually shows them all, so the data is clearly there.

A long list of twenty dashed empty rows with six solid filled rows in the middle, bracketed as “in the dom” against the whole column bracketed as “in the data”.
A virtualised list only ever puts the visible rows in the DOM. There is no “whole list” for a screenshot to photograph.

Why it happens

Long lists in modern applications are virtualised: only the rows near the viewport exist in the DOM, and scrolling recycles the same handful of elements with different data. The page is never in a state where all 5,000 rows exist, so no capture method can photograph them — the resize-and-snap approach cannot, and neither can scroll-and-stitch, because rows are being destroyed behind it as fast as they are created ahead of it.

The fix

This one has no clean workaround. In rough order of preference:

  • Use the application’s own export. If it can produce a CSV or a print view, that is a better artefact than a screenshot anyway.
  • Increase the page size. Many tables let you show 100 or 500 rows per page; more rows per capture means fewer captures.
  • Use the browser’s print view. Some applications render an unvirtualised list for @media print, which means Cmd+P gets the whole table when a screenshot cannot.
  • Capture in passes and stitch them yourself, accepting the overlap.

Hairlines and double edges: seams

A faint one-pixel line running horizontally across the image at regular intervals, or a row of text that appears twice with a few pixels of offset.

This is a rounding bug in the stitcher. Tiles are photographed in device pixels and placed in document space, and the conversion between the two rarely lands on whole numbers. Round a destination up and you leave a gap; round a size down and you leave a gap. The robust approach is to round destinations down and sizes up, so neighbouring tiles can only ever overlap by a pixel — drawing identical content twice, which is invisible — rather than leaving a hairline of background showing through.

The other half is the last tile. Browsers clamp scrolling at the bottom of the document, so asking for a scroll of exactly one viewport near the end lands somewhere short of it. A stitcher that assumes its request succeeded misplaces the final tile by the difference. Re-reading the actual scroll offset after each step is what keeps the bottom edge clean.

A capture that works first time

Thirty seconds of preparation removes most of the failures above. On any page you have not captured before:

StepFixes
Scroll to the bottom, wait, scroll back to the topBlank lazy-loaded images
Dismiss cookie banners, chat widgets and newsletter pop-upsOverlays stamped across the whole capture
Reset zoom to 100% with Cmd/Ctrl + 0Soft, low-resolution captures and unexpected layout
Turn on reduce-motion, or zero out animations from the consoleHalf-faded sections and mid-transition smears
Set the window to the width you actually care aboutA desktop capture used to argue about a mobile bug
Check whether any long list is virtualised before trusting the captureSilently missing rows
Six checks, in the order that costs the least time.

And if a capture still comes back wrong, work from the symptom rather than the tool. The page is doing something specific, and it has a name.

It is also worth asking whether an image was the right artefact in the first place. A virtualised table and a component you intend to rebuild both defeat screenshots for the same underlying reason — the four ways to capture a page covers what each format keeps and what it quietly discards.

Frequently asked questions

Why are the images blank in my full-page screenshot?

They were lazy-loaded and never requested. The capture method rendered the whole document without scrolling, so no image ever came near the viewport and no IntersectionObserver fired. Scroll to the bottom of the page, wait for the images to appear, scroll back to the top, then capture.

How do I stop the header repeating in a full-page screenshot?

The header is position: fixed or position: sticky, so it is genuinely in every viewport photograph. Use a tool that hides fixed elements during capture, or set them to position: absolute from the console beforehand — that pins the header to the top of the document so it appears exactly once.

What is the maximum height of a full-page screenshot?

Two limits apply in Chrome. A single-pass render is capped by the compositor’s maximum texture size, commonly 16,384 pixels. A 2D canvas — which is what stitching tools composite into — is capped at 32,767 pixels on a side, with a further limit on total area. Both are device pixels, so a HiDPI display halves the page height you can capture.

Why does my screenshot show a cookie banner over everything?

Cookie banners are usually fixed-position overlays, so they behave exactly like a sticky header and appear in every tile. Dismiss the banner before capturing. If it reappears on every visit because the page is in a private window, accept or reject it once in a normal window and capture there.

Can I capture content inside an iframe?

Same-origin iframes capture normally, because they render as part of the page. Cross-origin iframes — embedded videos, third-party widgets, payment forms — are captured as whatever the browser paints, which is usually correct, but their internal scroll position cannot be controlled from the parent page. Scroll the iframe’s content to where you want it before capturing.

Does a broken capture mean the extension is at fault?

Usually not. Blank images, repeated headers and truncation are all consequences of how the page is built or how tall it is. Seams and misplaced final tiles, on the other hand, are the tool’s arithmetic. A quick test: capture the same page with Chrome’s own DevTools command. If both fail the same way, it is the page.

Keep reading

A browser window with a long page beside five numbered method chips labelled DevTools, print to PDF, OS capture, extension and headless.
Screenshots

9 min read

How to Take a Full-Page Screenshot in Chrome: 5 Methods, Tested

Chrome will capture an entire scrolling page without any extension at all. It just hides the command, and gives up on pages past a certain height. Here are five routes and where each one stops working.

Diagram of one browser window fanning out into four capture formats: visible area, full page, PDF and HTML plus CSS.
Workflow

9 min read

How to Capture a Web Page: Screenshots, PDFs, and Code

A screenshot, a PDF and a copied component are three different answers to “save this page”. Here is what each one keeps, what each one silently throws away, and how to choose.

Two panels: a navigation bar with an open dropdown under a cursor, and the same bar with the cursor moved to a keyboard shortcut and the dropdown gone.
Screenshots

8 min read

How to Screenshot a Hover State, Dropdown, or Animation

Transient UI is the one thing a screenshot key cannot photograph, because taking the screenshot is what destroys it. Three escalating answers, from a checkbox in DevTools to not screenshotting at all.