Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use ElementHandle.screenshot() for a DOM element that extends below the viewport. Puppeteer scrolls the element into view and captures the element rather than requiring you to resize the browser. If you use a manual Page.screenshot() clip, obtain a valid bounding box and set captureBeyondViewport: true explicitly when the clip lies outside the viewport. A blank or partial image usually means the element is detached, has no layout box, or the page has rendering behavior (lazy loading, transforms, nested scrolling, sticky positioning) that must be handled before capture.

Use the element screenshot API first

Puppeteer’s documented element method is the shortest path:

“This method scrolls element into view if needed, and then uses Page.screenshot() to take a screenshot of the element.” — Puppeteer ElementHandle.screenshot() documentation, version 25.12.0.

The handle must still be attached to the document when the screenshot runs. The element can be taller or wider than the current viewport; you do not need to make the viewport as large as the element.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Complete JavaScript example

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  try {
    const page = await browser.newPage();
    await page.setViewport({width: 1365, height: 768, deviceScaleFactor: 1});
    await page.goto('https://example.com/page-with-a-long-panel', {
      waitUntil: 'networkidle2',
      timeout: 60_000,
    });

    const element = await page.waitForSelector('.target', {
      visible: true,
      timeout: 30_000,
    });
    if (!element) throw new Error('Target element was not found');

    await element.screenshot({path: 'element.png'});
  } finally {
    await browser.close();
  }
})();

Replace the URL and selector. waitForSelector confirms that the node exists and is visible, but it does not guarantee that images, fonts, or application data inside it have finished rendering. Add page-specific waits when those assets arrive after the initial navigation.

Wait for the content, not only the node

For a lazy-loaded panel, scroll it into view before waiting for its images, or wait for an application-ready marker:

await page.evaluate(() => {
  document.querySelector('.target')?.scrollIntoView({block: 'center'});
});
await page.waitForSelector('.target[data-ready="true"]');
await element.screenshot({path: 'ready-element.png'});

If the page has no ready marker, wait for specific images or a short, justified delay. A delay is less reliable than an observable condition because network and rendering times vary.

When to use a manual clip

Page.screenshot() is appropriate when you need an explicit coordinate rectangle, such as a region spanning several nodes. It is not the same as fullPage: true: fullPage captures the full page, while clip defines a rectangular area.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer’s current ScreenshotOptions documentation states that captureBeyondViewport defaults to false when no clip is supplied and true when a clip is supplied. Set it explicitly so your intent is clear and behavior is easier to diagnose.

const element = await page.waitForSelector('.target', {visible: true});
if (!element) throw new Error('Target element was not found');

const clip = await element.boundingBox();
if (!clip) throw new Error('Target element has no layout box');
if (clip.width <= 0 || clip.height <= 0) {
  throw new Error(`Target has an empty box: ${clip.width}x${clip.height}`);
}

await page.screenshot({
  path: 'clipped-element.png',
  clip,
  captureBeyondViewport: true,
});

boundingBox() returns coordinates relative to the main frame and pixel dimensions. It returns null when the node is not in layout—for example, when it or an ancestor is display: none, detached, or otherwise not renderable. Always check the result before passing it to screenshot.

Element method versus manual clip

Approach Target What you control Typical use
element.screenshot() One attached DOM element Puppeteer finds the element’s rendered bounds and scrolls it into view Capturing a card, panel, report, or component
page.screenshot({clip, captureBeyondViewport: true}) Coordinate rectangle Exact x, y, width, and height, plus beyond-viewport behavior Combining areas or applying a calculated crop
page.screenshot({fullPage: true}) Whole document Full page rather than one element Entire-page archives, not an element-specific fix

Diagnose blank space or a blank lower section

1. Confirm the handle is still attached

Single-page applications can replace a node after your selector resolves. Re-query immediately before capture, and avoid retaining a handle across navigation or a component rerender.

const element = await page.waitForSelector('.target', {visible: true});
const attached = await element.evaluate(node => node.isConnected);
if (!attached) throw new Error('Target was detached before capture');

If this check fails, wait for the rerender to finish and obtain a fresh handle.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Check the layout box

A null, zero-width, or zero-height box means there is no drawable layout region. Inspect computed styles and ancestors:

const details = await element.evaluate(node => {
  const style = getComputedStyle(node);
  const rect = node.getBoundingClientRect();
  return {
    connected: node.isConnected,
    display: style.display,
    visibility: style.visibility,
    width: rect.width,
    height: rect.height,
  };
});
console.log(details);

Fix the page state—such as opening a collapsed section or removing a temporary hidden class—before taking the screenshot.

3. Distinguish clipping from page rendering

If the upper part is correct but the area below the viewport is empty, compare the two capture paths. A successful element.screenshot() indicates that the DOM element can be captured; a manual clip that differs points to its coordinates or options. For a manual clip, use the current bounding box and captureBeyondViewport: true.

4. Account for lazy content and nested scrolling

Lazy images may not load until their own scroll container is moved. Scroll the relevant container, wait for image completion, and then capture:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.evaluate(() => {
  const panel = document.querySelector('.scroll-panel');
  if (panel) panel.scrollTop = panel.scrollHeight;
});
await page.waitForFunction(() => {
  const images = [...document.querySelectorAll('.target img')];
  return images.every(img => img.complete && img.naturalWidth > 0);
});
await element.screenshot({path: 'loaded-panel.png'});

This is page-specific: a virtualized list may render only visible rows, so no screenshot setting can capture rows that the application has not created. Render or expand those rows first.

5. Check transforms, fixed positioning, and sticky elements

CSS transforms can make visual coordinates differ from ordinary layout expectations. Fixed and sticky descendants may appear in a different position while Puppeteer scrolls the target. Capture a stable state, temporarily disable animation, and test whether the problem disappears:

await page.addStyleTag({content: `
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    caret-color: transparent !important;
  }
`});

Do not assume this fixes every blank capture; it merely removes timing changes while you investigate.

Why resizing the viewport is not a universal fix

A historical Puppeteer issue, issue #1779, reported an oversized element being clipped in version 0.13.0 and discussed enlarging the viewport as a workaround. The report also noted possible media-query and resize-event side effects. That discussion is historical, not a current guarantee.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Puppeteer changelog records an element-screenshot viewport-setting change in 21.9.0 and removal of viewport resizing from ElementHandle.screenshot() in 23.9.0 (2024-11-21). Therefore, avoid treating viewport enlargement as the default remedy. It can change responsive layouts, trigger resize handlers, increase memory use, and produce a different page from the one users see. Check the Puppeteer and Chromium versions actually installed by your project before attributing behavior to a particular release; the current API material identifies Puppeteer 25.12.0.

Reliable capture checklist

  • Launch the same Puppeteer and Chromium versions used in production.
  • Set the intended viewport and device scale factor before navigation.
  • Wait for the selector and for the component’s data and assets.
  • Re-query after SPA navigation or component rerenders.
  • Verify isConnected, a non-null bounding box, and positive dimensions.
  • Use ElementHandle.screenshot() for one element.
  • For a manual crop, pass a fresh clip and explicitly set captureBeyondViewport: true.
  • Disable animations when deterministic pixels matter.
  • Investigate lazy loading, virtualization, nested scroll containers, transforms, and sticky or fixed descendants.
  • Save diagnostic values (URL, selector, viewport, box, and installed versions) with failed jobs.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One request returns a PNG, JPEG, WebP, or PDF, and it can capture a CSS-selected element without you managing Puppeteer or Chromium. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

For the one-call version, see the ScreenshotNeo API documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

It also supports full-page captures with lazy images loaded, CSS selectors, custom waits, viewport and device presets, retina scale, custom CSS and JavaScript, click actions, hidden selectors, request blocking, cookies and headers, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. The parameter names used by other screenshot APIs also work, which can simplify migration.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing provides two months free. Create a free ScreenshotNeo account to try it.

Python and Node.js alternatives for ScreenshotNeo

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Frequently Asked Questions

Does fullPage capture an oversized element?

No. fullPage targets the whole document. Use ElementHandle.screenshot() for one element or a manual clip for a rectangle.

What does a null bounding box mean?

Puppeteer could not find a layout box, commonly because the node is detached, hidden, or not laid out. Wait for the component to render and verify its styles and dimensions.

Should I upgrade Puppeteer to fix a blank screenshot?

Check the installed Puppeteer and Chromium versions first. The documented APIs do not establish one universal version upgrade that fixes every blank or clipped element.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.