How to Scroll a Page with Puppeteer Before Taking a Screenshot
Scroll the document or a nested container with Puppeteer, wait for page changes, and capture the viewport, full page, or a single element.
To scroll the document and capture the viewport at the new position, run window.scrollTo() inside page.evaluate(), then call page.screenshot():
await page.evaluate(() => window.scrollTo(0, 600));
await page.screenshot({ path: 'page.png' });
Change 600 to the vertical position in CSS pixels you want to show. If the page scrolls inside a nested element, scroll that element instead. To capture the entire document, set fullPage: true; that is different from capturing the viewport after scrolling.
1. Set up Puppeteer
Install Puppeteer in a Node.js project, then save the following as scroll-shot.js. The script opens a URL, scrolls to a chosen position, and writes a viewport screenshot.
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1280, height: 800 },
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => window.scrollTo(0, 600));
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
})();
Run it with node scroll-shot.js. Replace the example URL with a page you are authorized to access. This uses CommonJS, which works in a standard Node project; in an ES module project, use import puppeteer from 'puppeteer'; and retain the same browser calls.
2. Scroll the document to a position
Page.evaluate() runs a function in the browser page context. Use it to invoke the browser’s window.scrollTo(x, y) API:
await page.evaluate(() => window.scrollTo(0, 600));
The first coordinate is horizontal and the second is vertical. To scroll relative to the current position instead, use window.scrollBy():
await page.evaluate(() => window.scrollBy(0, 500));
For a robust script, check the resulting position and clamp a requested target to the document’s available scroll range:
const requestedY = 1200;
const actualY = await page.evaluate((y) => {
const maxY = document.documentElement.scrollHeight - window.innerHeight;
const targetY = Math.max(0, Math.min(y, maxY));
window.scrollTo(0, targetY);
return window.scrollY;
}, requestedY);
console.log('Scrolled to', actualY);
await page.screenshot({ path: 'page.png' });
If the document is shorter than the requested position, the browser cannot scroll to that position; the returned scroll position makes this visible to the script.
3. Scroll a nested container
When a panel, feed, or modal has its own scrollbar, scrolling the window may leave that content unchanged. Puppeteer’s locator API can scroll a specific element using mouse-wheel events:
await page.locator('.scroll-container').scroll({ scrollTop: 600 });
await page.screenshot({ path: 'container-view.png' });
scrollTop sets the vertical scroll position and scrollLeft can set the horizontal position. Locator scrolling checks that the target is visible and its bounding box remains stable over two animation frames before scrolling. Select a locator that uniquely identifies the intended scrollable region. If the page has several matching panels, use a more specific selector.
const panel = page.locator('[data-testid="results-panel"]');
await panel.scroll({ scrollTop: 400, scrollLeft: 0 });
await page.screenshot({ path: 'results-panel.png' });
4. Choose the screenshot region
Capture the viewport after scrolling
Call page.screenshot() after the scroll. By default, fullPage is false, so the output is the current viewport:
await page.evaluate(() => window.scrollTo(0, 600));
await page.screenshot({ path: 'viewport.png' });
Capture the entire document
Use fullPage: true when you want the full document, rather than only the currently visible region:
await page.screenshot({ path: 'full-page.png', fullPage: true });
This option changes the capture region; it does not mean “capture only the viewport after scrolling.”
Capture one element
For a single element, obtain an element handle and call its screenshot method. Puppeteer scrolls the element into view if needed, then captures that element:
const element = await page.$('.report-card');
if (!element) throw new Error('Report card was not found');
await element.screenshot({ path: 'report-card.png' });
Element capture can therefore move the page as part of bringing the target into view. Use it when the output should be the element itself, rather than a viewport at a deliberately chosen scroll position.
5. Wait for content that changes after scrolling
A scroll can trigger lazy images, infinite-scroll results, animations, or other asynchronous updates. Scrolling does not guarantee that those changes are ready before the screenshot. Wait for a condition that reflects the page’s actual state with page.waitForFunction().
await page.evaluate(() => window.scrollTo(0, 900));
await page.waitForFunction(
() => document.querySelector('[data-loaded="true"]') !== null,
{ timeout: 10000 }
);
await page.screenshot({ path: 'loaded-section.png' });
Replace the selector and condition with a signal the site really provides, such as a loading indicator disappearing or a result count changing. The wait options support request-animation-frame polling, DOM-mutation polling, or a numeric interval. The documented default timeout is 30 seconds; set a shorter timeout when a slow or broken page should fail promptly.
For lazy-loaded images, scroll near the relevant content and wait for the image to finish loading before capture:
await page.evaluate(() => window.scrollTo(0, 1200));
await page.waitForFunction(() => {
const img = document.querySelector('.target-image');
return img && img.complete && img.naturalWidth > 0;
});
await page.screenshot({ path: 'image-section.png' });
Use a page-specific timeout in production so a missing image or selector does not leave a job waiting indefinitely.
6. Complete runnable example with a readiness check
This version checks navigation, scrolls, waits for a target image, captures the viewport, and closes the browser even if an operation fails:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1280, height: 800 },
});
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
await page.evaluate(() => window.scrollTo(0, 1200));
await page.waitForFunction(
() => {
const image = document.querySelector('.target-image');
return !image || (image.complete && image.naturalWidth > 0);
},
{ timeout: 10000 }
);
await page.screenshot({ path: 'page.png' });
} finally {
await browser.close();
}
})();
The sample treats the image as optional; change the condition if the image must exist. Make the readiness check match the page under automation instead of assuming that a fixed delay guarantees completion.
7. cURL, Python, and ScreenshotNeo
Puppeteer is a Node.js browser automation library, so its scroll-and-capture calls run in JavaScript. If you need a screenshot from a shell or Python service without managing a browser, ScreenshotNeo provides a screenshot API. Its API accepts a URL in one GET request; the following captures a page as WebP. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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()
with open("shot.webp", "wb") as output:
output.write(r.content)
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
These API examples capture the requested page; they do not expose Puppeteer’s arbitrary browser-side window.scrollTo() call. ScreenshotNeo also supports full-page and element capture, viewport and device settings, wait conditions, custom CSS and JavaScript, and other capture options. See the docs for the available parameters and their exact names.
Or skip the browser setup
With ScreenshotNeo, one GET request returns a screenshot or PDF. Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, timeouts, and failed loads are never billed, and cache hits cost nothing. An MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Read the API docs and sign up for 1,000 free screenshots a month, with no card.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot shows the top of the page | The scroll ran in the wrong context, did not complete, or the page scrolled inside a nested container. | Run window.scrollTo() through page.evaluate() for document scrolling. For an inner panel, use locator.scroll() on that panel. Read window.scrollY to confirm document position. |
| The requested scroll position is not reached | The document is shorter than the requested position, or content has not yet expanded. | Compute the maximum from document.documentElement.scrollHeight - window.innerHeight, clamp the target, and wait for content expansion if applicable. |
| New content is missing from the image | Lazy loading or an asynchronous page update is still in progress. | Wait for a page-specific selector, state change, or image load with waitForFunction() before capture. |
| Locator scroll fails or targets the wrong panel | The selector may match no element, multiple elements, or an element that is hidden or unstable. | Use a unique selector and ensure the container is visible and scrollable. Check that the chosen element owns the scrollbar. |
| Only the visible part of a long page appears | fullPage defaults to false. |
Set fullPage: true for the whole document, or retain the default for a scrolled viewport. |
| The element screenshot changes the scroll position | Element screenshot automatically scrolls its target into view. | Use a viewport screenshot if the precise viewport position matters; use element capture when the element itself is the intended output. |
| The script hangs waiting for readiness | The chosen condition never becomes true or has no timeout. | Choose a condition tied to the page’s real state and set a finite timeout with an actionable error path. |
9. Performance, reliability, and cost
- Wait for the needed signal: A condition tied to the content is more reliable than an arbitrary pause. Keep a finite timeout and report which condition failed.
- Capture only the required region: A viewport or element image is usually smaller than a full-page image. Full-page captures of very long pages can require more browser work and produce large files.
- Close browser resources: Put
browser.close()in afinallyblock so failed navigations or waits do not leave browser processes running. - Keep output and navigation bounded: Set navigation and condition timeouts. Handle pages that never finish loading as failures rather than waiting forever.
- Plan cost around your execution environment: A self-managed Puppeteer script has no ScreenshotNeo API charge, but you operate the browser process and its compute resources. ScreenshotNeo pricing is Free for 1,000 shots/month, Starter $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. Every feature is on every plan. Only clean shots are billed; response headers identify the page verdict and billing status.
10. Frequently asked questions
Does page.screenshot() scroll the page for me?
No. Scroll the document or container first when you need a particular viewport. Element screenshots are the exception: Puppeteer scrolls the target into view when needed.
Can I scroll horizontally?
Yes. Use a nonzero first coordinate with window.scrollTo(x, y), or set scrollLeft when scrolling a locator.
Which Puppeteer version should I use?
Check the API reference for the version installed in your project. Method signatures and behavior can change across releases.
Can I use Puppeteer to capture a PDF instead?
Yes. Puppeteer has page PDF capture APIs; consult the documentation for your installed version and the print options your output requires.


