ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Cuts Off Text at the Bottom of Each Section

Diagnose clipped Puppeteer screenshots by checking capture options, nested scrolling, CSS overflow, timing, and viewport changes—with runnable fixes.

By the ScreenshotNeo team4 October 20269 min read

If a Puppeteer screenshot cuts off text at the bottom of each section, first check what is being captured and which element actually scrolls. fullPage: true captures the document beyond the viewport; it does not expand every independently scrolling section. Inspect the screenshot options, nested scroll containers, clipping CSS, and layout timing before changing the viewport or styles.

The exact cause depends on your screenshot call, page structure, CSS, and Puppeteer/Chromium versions. The steps below distinguish viewport clipping, document capture, element capture, and inner scrolling so you can choose a fix that preserves the layout you intend to capture.

1. Check the screenshot call and installed version

Start with the exact options passed to page.screenshot() or elementHandle.screenshot(). Puppeteer’s current API defines fullPage: true as a screenshot of the full page. A clip specifies a region, and captureBeyondViewport controls capture beyond the viewport; the documented default is false when there is no clip and true when there is one. See the ScreenshotOptions API. Its published page currently displays Puppeteer 25.12.0; check the docs matching your installed version because older versions and their bundled browsers may behave differently.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 800 });
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });

    // Full document screenshot, not just the current viewport.
    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Install and version-check the package in your project with npm ls puppeteer. If you use puppeteer-core, also establish which Chrome or Chromium executable is launched. Record both versions when reducing a version-specific problem.

2. Find the element that owns the scrollbar

A full-page screenshot follows the main document. It does not automatically capture all content inside a section whose own fixed-height box scrolls. Check whether the page scrollbar moves, or whether a panel/section has its own scrollbar. Compare scrollHeight with clientHeight on the suspected element and inspect its ancestors for clipping.

const report = await page.evaluate(() => {
  return [...document.querySelectorAll('body *')]
    .map(el => {
      const s = getComputedStyle(el);
      const r = el.getBoundingClientRect();
      return {
        tag: el.tagName,
        id: el.id,
        className: typeof el.className === 'string' ? el.className : '',
        height: Math.round(r.height),
        clientHeight: el.clientHeight,
        scrollHeight: el.scrollHeight,
        overflowY: s.overflowY,
        overflowX: s.overflowX,
        maxHeight: s.maxHeight
      };
    })
    .filter(x => x.scrollHeight > x.clientHeight + 1 ||
                 ['hidden', 'auto', 'scroll', 'clip'].includes(x.overflowY));
});
console.table(report);

Look for a constrained height or max-height combined with overflow: hidden, auto, or scroll. A difference between scrollHeight and clientHeight indicates content extends beyond the visible box. It does not by itself prove that this is the screenshot bug; verify which box is meant to scroll.

3. Choose a capture method for the intended output

Capture the full document

When the main page document is taller than the viewport and you want the entire document, use fullPage: true. Remove an unintended clip if it restricts the output. This will not reveal content clipped within nested containers.

await page.screenshot({ path: 'full-document.png', fullPage: true });

Capture one element

Use ElementHandle.screenshot() when the intended result is a particular element. Puppeteer’s guide says this method scrolls a hidden element into view; the current API describes it as scrolling the element into view if needed and then delegating to page screenshot capture. Scrolling into view does not remove CSS clipping inside that element. See the Puppeteer screenshot guide and ElementHandle.screenshot API.

const panel = await page.$('.report-panel');
if (!panel) throw new Error('Could not find .report-panel');
await panel.screenshot({ path: 'panel.png' });

Capture the scrollable contents of a nested section

If the section itself scrolls and the goal is to show all its content, temporarily expand the relevant box for capture. This changes page styling, so use it only if an expanded panel represents the screenshot you need. A generic helper can remove the height constraint and reveal overflow on the chosen element; adapt the selector and properties to the site’s CSS.

const selector = '.report-panel';
await page.$eval(selector, el => {
  el.dataset.captureOriginalStyle = el.getAttribute('style') || '';
  el.style.setProperty('height', 'auto', 'important');
  el.style.setProperty('max-height', 'none', 'important');
  el.style.setProperty('overflow', 'visible', 'important');
});

const panel = await page.$(selector);
await panel.screenshot({ path: 'expanded-panel.png' });

This inline-style example records the original style text but does not restore it automatically. If the page will be reused after capture, save and restore the original attributes in a finally block. Also inspect ancestor elements: an ancestor with clipping overflow can still cut off the expanded content.

Use a clip only when a bounded region is intended

A clip is a fixed rectangle, so it can deliberately omit content outside its bounds. Check the coordinates and dimensions if you use one. For a full document capture, omit the clip and request fullPage instead.

await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 900, height: 1200 }
});

4. Treat viewport resizing as a conditional workaround

Increasing viewport height can make more content visible to an element screenshot, but it can also change the page itself. Viewport-relative units such as vh, responsive breakpoints, fixed or sticky elements, lazy loading, and page shifts can all produce a different layout. Puppeteer maintainer discussion describes viewport clipping behavior introduced with the Chromium roll in Puppeteer v2.0.0 and explains these layout tradeoffs. Treat a taller viewport as an experiment, not a universal fix, and compare the rendered page before and after resizing. See the Puppeteer maintainer discussion.

const dimensions = await page.evaluate(() => ({
  viewportHeight: innerHeight,
  documentHeight: document.documentElement.scrollHeight
}));

// Only do this if a taller viewport matches the desired capture semantics.
await page.setViewport({ width: 1280, height: dimensions.documentHeight });
await page.screenshot({ path: 'tall-viewport.png' });

A very tall viewport may be impractical for a long page. It may also trigger different responsive behavior, and a screenshot without fullPage: true can still represent only the resulting viewport. Prefer the capture mode that expresses your intent.

5. Wait for the layout and content to settle

Navigation completing does not guarantee that application-rendered content has reached its final size. The official guide uses waitUntil: 'networkidle2' in an example, but that is not a guarantee for every site. Wait for the site’s own readiness signal, a known selector, or stable dimensions when your reproduction shows content is still changing.

await page.goto('https://example.com/report', { waitUntil: 'networkidle2' });
await page.waitForSelector('.report-panel[data-ready="true"]');

const before = await page.$eval('.report-panel', el => el.scrollHeight);
await new Promise(resolve => setTimeout(resolve, 250));
const after = await page.$eval('.report-panel', el => el.scrollHeight);
if (before !== after) {
  throw new Error(`Panel is still changing size: ${before} -> ${after}`);
}

await page.screenshot({ path: 'report.png', fullPage: true });

Replace the selector and readiness attribute with an application-specific condition. A fixed delay alone is less reliable: it can be unnecessarily long on fast runs and still too short on slow ones.

6. Investigate a narrow overflow and border edge case only when it matches

Puppeteer issue #7578 reports a particular full-page screenshot case at 600px viewport width where a final horizontally scrollable pre block appeared empty with a combination of overflow and border styles. That report is a reproduction to test, not a general explanation for text clipped at section bottoms. Compare the affected viewport width, isolate the relevant element and CSS, and reduce the case to minimal HTML before attributing your issue to it. See Puppeteer issue #7578.

7. Troubleshooting checklist

Symptom Likely cause to check Next step
Only the visible viewport is present The call omits fullPage: true, or uses a clip. Inspect the exact options; request a full-page capture if the document is the target.
The page is complete, but panels are clipped A nested scroll container has a constrained height or clipping overflow. Find the actual scrolling element; capture its visible state or expand it deliberately.
An element shot is cut at its own boundary Element capture scrolls into view but does not undo CSS overflow clipping. Inspect the element and clipping ancestors; choose a visible-state or expanded-content capture.
Changing viewport height fixes one page but breaks another vh layout, breakpoints, fixed/sticky positioning, or lazy loading changed. Compare layout at both sizes and use resizing only when the changed layout is acceptable.
Text or images appear only sometimes Content or layout may still be loading or shifting. Wait for an application-specific ready condition and confirm dimensions are stable.
A horizontally scrolling code block is blank near the page end Potential match for the issue #7578 overflow/border reproduction. Test a minimal reproduction at the same width and isolate the CSS combination.
Advice from current docs does not match runtime Installed Puppeteer or browser version differs. Record package and browser versions, then consult the corresponding API behavior.

8. Performance, reliability, and cost

Full-page screenshots can produce large image files and require more rendering and encoding work as page dimensions grow. Capture only the required document, element, or region when that matches the use case. Expanding a nested section or changing viewport size can force layout recalculation and may trigger more lazy content, so wait for the resulting page state before capturing.

For reliable automation, pin or record Puppeteer and browser versions, use explicit viewport dimensions, wait on application readiness rather than relying solely on a generic network condition, and preserve a small reproducible page when investigating rendering differences. There is no single CSS change that can be recommended without knowing which element scrolls and whether clipping is intentional.

Self-hosted Puppeteer has no per-screenshot API fee, but your automation still uses compute, memory, storage, and maintenance time. Large pages and repeated captures increase those resource demands. If the main task is obtaining screenshots rather than controlling a local browser session, a screenshot API can avoid operating the browser infrastructure yourself.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its clean-shot flow accepts cookie/consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report the page verdict and billing status in headers. AI agents can use its MCP server tools: take_screenshot, get_page_info, and capture_pdf.

Use the API docs for the ScreenshotNeo request options and configuration. This runnable cURL example saves a WebP screenshot:

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

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 request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

It also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and custom viewports, retina scale, PDF page settings, HTML/CSS-to-image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, ad/tracker/request/resource blocking, headers, cookies, user agent and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable cache TTL, signed image links, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI spec. Parameter names used by other screenshot APIs also work to ease migration.

All features are on every plan: Free includes 1,000 screenshots/month with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. For the specific clipping issue, verify that the target is the main document or a nested scrolling element, then select the corresponding capture behavior.

Create a free account for 1,000 screenshots a month with no card.

FAQ

Does fullPage: true capture content hidden inside every section?

No. It captures the full page document; a nested scrolling section may still show only its visible box.

Will ElementHandle.screenshot() expand an element?

No. It scrolls the element into view when needed. CSS clipping inside the element or its ancestors can remain.

Should I always increase the viewport height?

No. Resizing can change layout, fixed and sticky positioning, responsive rules, and lazy loading. Test it against the intended output.

What details help diagnose a remaining clipping bug?

Share a minimal HTML/CSS reproduction, the screenshot options, the element that scrolls, the readiness condition, and Puppeteer and browser versions.