How to Capture Screenshots of a Page with Infinite Scroll and a Footer
A full-page screenshot captures what has loaded so far. Scroll an infinite page until its footer appears, then capture and check the result.
A full-page screenshot captures the page’s scrollable content at capture time; it does not automatically load every item in an infinite feed. To include the footer, first scroll the page in increments until the footer appears or the page stops growing, let the layout settle, then capture the full page and inspect the saved image.
With Playwright, the final capture call is await page.screenshot({ path: 'screenshot.png', fullPage: true }). The important step for infinite scroll is the scrolling that happens before that call. See the Playwright screenshots guide and Page API reference.
1. Load the infinite content before capturing
Many pages fetch more items when the visitor approaches the bottom. A screenshot option cannot know how much of an intentionally endless feed you want, so set a stopping rule: stop when the footer is visible and the page height is stable, or after a chosen number of scrolls/items for feeds without a footer.
- Navigate and wait for the initial page content.
- Scroll roughly one viewport at a time, allowing the page to fetch and render more content.
- Repeat while the document grows. Stop when the footer is visible and the document height remains stable, or when your explicit scroll/item limit is reached.
- Wait for late layout changes and lazy images, then take a full-page screenshot.
- Open the image and verify the footer, final expected content, and image loading.
The loop below uses document height and footer visibility as practical signals. It is not a guarantee that every site’s loading behavior can be detected: a page can load content without changing its height, use a nested scroll panel, or keep producing feed items indefinitely.
2. Playwright: complete runnable example
Install Playwright and its Chromium browser in a Node.js project:
npm install playwright
npx playwright install chromium
Save this as capture.mjs, then run node capture.mjs https://example.com. Replace the URL with a page you are allowed to access.
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) throw new Error('Usage: node capture.mjs <url>');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('body').waitFor({ state: 'visible', timeout: 15_000 });
const footer = page.locator('footer').first();
const maxScrolls = 40;
const stableRoundsNeeded = 3;
let stableRounds = 0;
let previousHeight = 0;
let footerSeen = false;
for (let i = 0; i < maxScrolls; i++) {
const state = await page.evaluate(() => ({
height: document.documentElement.scrollHeight,
viewport: window.innerHeight,
y: window.scrollY
}));
await page.evaluate(() => window.scrollBy(0, Math.max(1, window.innerHeight * 0.85)));
await page.waitForTimeout(700); // Adjust for the site's load/render time.
footerSeen = await footer.count().then(async count =>
count > 0 && await footer.isVisible().catch(() => false)
);
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
if (newHeight === previousHeight && newHeight === state.height) {
stableRounds++;
} else {
stableRounds = 0;
}
previousHeight = newHeight;
if (footerSeen && stableRounds >= stableRoundsNeeded) break;
if (state.y + state.viewport >= newHeight && stableRounds >= stableRoundsNeeded) break;
}
// Return to the top so the full-page image starts at the beginning.
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
await page.screenshot({ path: 'screenshot.png', fullPage: true });
if (!footerSeen) {
console.warn('Footer was not detected; inspect screenshot.png and adjust the selector or stopping rule.');
}
} finally {
await browser.close();
}
The selector footer is only a convention; some sites use another element or no semantic footer tag. Set a site-specific locator such as page.locator('#site-footer') when needed. The script uses a bounded number of iterations to avoid chasing a feed forever. If the page has no footer, choose a fixed maximum scroll count or a target item selector and stop when that target appears.
3. Puppeteer alternative
For an existing Puppeteer project, use the same load-then-capture sequence. Install with npm install puppeteer; Puppeteer downloads a compatible browser during installation in its standard setup.
import puppeteer from 'puppeteer';
const url = process.argv[2];
if (!url) throw new Error('Usage: node capture-puppeteer.mjs <url>');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900 });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
let previousHeight = 0;
let stable = 0;
const maxScrolls = 40;
for (let i = 0; i < maxScrolls; i++) {
await page.evaluate(() => window.scrollBy(0, Math.max(1, window.innerHeight * 0.85)));
await new Promise(resolve => setTimeout(resolve, 700));
const height = await page.evaluate(() => document.documentElement.scrollHeight);
stable = height === previousHeight ? stable + 1 : 0;
previousHeight = height;
const footerVisible = await page.$eval('footer', el => {
const r = el.getBoundingClientRect();
return r.top < window.innerHeight && r.bottom > 0;
}).catch(() => false);
if (footerVisible && stable >= 3) break;
}
await page.evaluate(() => window.scrollTo(0, 0));
await new Promise(resolve => setTimeout(resolve, 500));
await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
await browser.close();
}
Puppeteer documents page and element screenshots, including the fullPage screenshot option, in its screenshots guide and ScreenshotOptions reference.
4. Adjust the capture for the page
Choose the right stopping condition
- Footer exists: wait until it is in view and height has stayed stable for several rounds. If the footer is rendered only after another request, increase the per-scroll pause or wait for a site-specific footer condition.
- Endless feed: use a maximum number of scrolls, elapsed time, or target item count. There is no finite “all content” endpoint for a feed that keeps generating items.
- Content changes without height growth: compare an item count or last-item identifier as well as document height. Height alone can miss replacements, virtualized lists, or same-height updates.
- Lazy images: scrolling generally gives images an opportunity to enter the viewport and load. Before capture, wait for a known image selector or check the images you care about; do not assume network idle means every lazy image is ready.
Nested scroll containers
If only a panel scrolls, window.scrollBy will not trigger its loading behavior. Identify the panel and scroll it instead, for example by evaluating panel.scrollTop += panel.clientHeight * 0.85 repeatedly. Capture the whole page only if the panel’s expanded content becomes part of the document; otherwise capture the panel element with an element screenshot or adjust the page layout for the intended output.
Pick page, element, or viewport capture
- Full page: captures the document’s scrollable extent as it exists when the screenshot is taken.
- Viewport: captures only what is currently visible; useful for a particular feed position.
- Element: captures a component such as the footer or feed. Both Playwright and Puppeteer document element screenshots.
A very tall full-page image can be unwieldy. If only the footer and final content matter, a viewport or element capture may be more useful than one enormous image.
5. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Footer is missing | The screenshot ran before the scroll-triggered content loaded, the page uses a different footer selector, or the footer is in a nested panel. | Scroll in increments before capture, increase the settling delay, use the correct footer locator, and scroll the actual container. |
| Only the initially loaded cards appear | fullPage: true captures existing content but does not itself trigger infinite-scroll requests. |
Run a bounded scroll loop first and confirm item count or footer visibility. |
| Images are blank or partially loaded | Lazy loading or image decoding had not finished at capture time. | Scroll images into view, wait for a relevant image condition, then inspect the output. |
| The loop never stops | The feed is endless or content keeps increasing the document height. | Set a maximum scroll count/time/item target and report that the result is a bounded capture. |
| Height stabilizes too early | Requests are delayed, content replaces existing rows, or the page uses virtualization. | Increase the pause and monitor a content-specific signal such as item count or last item text in addition to height. |
| Repeated header or sticky elements look wrong | Full-page capture may interact with fixed/sticky positioning in a site-specific way. | Inspect the image; hide the sticky selector for the capture or use viewport/section captures if suitable. |
| Navigation times out | The page remains active, has slow resources, or never reaches the chosen load state. | Use a suitable navigation wait condition such as domcontentloaded, set a realistic timeout, then wait for the actual content selector. |
6. Performance, reliability, and cost
Each scroll, pause, and screenshot adds time. Keep the viewport and scroll increment large enough to make progress, but leave enough pause for the site’s request and rendering cycle. A bounded loop makes duration and output size predictable; full-page images of long feeds can consume substantial memory and create large files, so consider a maximum depth or sectional captures.
For repeatable jobs, use a stable viewport, explicit timeouts, site-specific selectors, and a logged stopping reason (footer found, stable height, target count, or limit reached). Save the screenshot and inspect it when correctness matters. Sites can change markup, require authentication, throttle automation, or vary content, so the same generic loop cannot guarantee identical results everywhere.
Playwright and Puppeteer are software libraries; this method has no per-capture API fee, though it uses your own compute, browser installation, and network. The sources document the screenshot APIs, not universal speed or reliability claims.
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API: send a URL in one GET request and receive an image or PDF. Its full-page option loads lazy images, but an infinite feed still needs a finite stopping point; choose a target URL whose page has a footer after its intended content is loaded.
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)
open("shot.webp", "wb").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}`);
See the ScreenshotNeo API documentation for the full-page parameter and other options. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for the free plan.
8. FAQ
Does fullPage: true load every infinite-scroll item?
No. It captures the full scrollable area that exists at capture time. Scroll first to trigger the page’s loading behavior.
How do I know when to stop scrolling?
Use footer visibility plus several stable height checks when a footer exists. For an endless feed, choose a maximum count, time, or item target.
Can I capture just the footer?
Yes. Use an element screenshot with a footer selector when the footer is present in the DOM and you only need that component.
Why can a page height check miss new content?
Some pages replace items, virtualize rows, or load content into a fixed-size panel. Track a content-specific signal and scroll the actual container.


