How to Fix Screenshot Monitoring That Misses Changes Below the Fold
When screenshot monitoring misses below-the-fold changes, check capture scope, trigger lazy content, wait for readiness, and stabilize the comparison.
If a screenshot monitor misses a change below the fold, first check what it captures: a viewport, an element or clipped region, or the full page. Then make the page load the content you need to compare, wait for evidence that it is ready, and capture the intended surface. A full-page option changes the capture extent; it does not guarantee that lazy-loaded or interaction-gated content has rendered.
This guide shows how to diagnose the capture, trigger below-the-fold content, and take repeatable screenshots with Playwright, Cypress, and Puppeteer. It also covers common causes, trade-offs, and ways to reduce noisy visual diffs.
1. Confirm what the monitor is capturing
Save or inspect the actual screenshot artifact before changing the test. Record the framework, viewport dimensions, capture mode, and whether the image contains test-runner chrome. A monitor can be working as configured while only saving the visible viewport.
| Capture surface | What it covers | Use it when |
|---|---|---|
| Viewport | The currently visible browser area | The behavior under test fits in one screen |
| Element or clipped region | A selected component or rectangle | You are checking a specific component and want to avoid unrelated page changes |
| Full page | The page’s scrollable document | The contract covers layout across the whole page |
| Runner view | In applicable Cypress configurations, the application plus Cypress Command Log | You specifically need the test-runner view |
Playwright and Puppeteer default to viewport screenshots: set fullPage: true for a full-page capture. Cypress supports viewport, fullPage, and runner capture modes. Check your installed framework version and the artifact your monitoring system actually compares.
2. Trigger the content below the fold
Many pages defer images, cards, embeds, and other content until they approach the viewport. A full-page screenshot may cover the page’s full dimensions while still capturing sections that have not loaded. Scroll through the relevant areas or perform the interaction that reveals them before taking the snapshot.
For a simple page, scrolling to the bottom and waiting for images can be enough. For incremental loading, nested scroll containers, infinite scroll, or interaction-gated content, use a controlled sequence tailored to the page. Scrolling the document does not necessarily scroll an inner container; identify and scroll the container that owns the content.
Prefer a condition that demonstrates readiness over a fixed sleep. For images, useful checks include img.complete and img.naturalWidth > 0. Those checks apply to images; for other content, wait for the expected element, application state, or relevant response.
3. Make the screenshot repeatable
- Use stable test data or fixtures so changing responses do not create unrelated diffs.
- Wait for the intended application state and below-the-fold content before comparing images.
- Use a consistent rendering environment, viewport, and browser where possible.
- Mask a small, genuinely dynamic region when needed instead of raising a page-wide failure threshold.
- Choose an element snapshot when the regression contract concerns one component rather than the whole page.
Playwright’s toHaveScreenshot waits for two consecutive screenshots to match before comparing with the expected image. That can help with transient visual changes, but it does not trigger lazy content or prove the application reached the state you intend to test.
4. Runnable Playwright example
This example uses the Playwright test runner. It scrolls the document in steps to trigger viewport-based loading, waits for images to finish, checks a page-specific section, and takes a full-page screenshot. Replace the URL and readiness selector with values from the page under test.
import { test, expect } from '@playwright/test';
test('captures below-the-fold content', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Scroll in viewport-sized steps so lazy content can approach the viewport.
await page.evaluate(async () => {
const step = Math.max(1, window.innerHeight - 100);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
// This image-specific check does not establish readiness for other widgets.
await page.waitForFunction(() =>
[...document.images].every(img => img.complete && img.naturalWidth > 0)
);
// Replace with a selector that proves your target section is ready.
await expect(page.locator('[data-testid="pricing-section"]')).toBeVisible();
await page.screenshot({ path: 'page.png', fullPage: true });
});
If the page intentionally contains broken or optional images, do not wait for every image to have a nonzero natural width. Filter to the required images or wait for the application-specific section state instead. For a visual assertion, the test runner can compare a stable screenshot:
await expect(page).toHaveScreenshot('page.png', { fullPage: true });
Use the installed Playwright version’s documentation for assertion options and behavior. See the Playwright Page API and PageAssertions API.
5. Cypress and Puppeteer patterns
Cypress
Trigger lazy content before saving a full-page screenshot. Cypress full-page capture scrolls from top to bottom and stitches the captures; fixed or sticky elements may appear more than once.
cy.visit('https://example.com');
cy.get('[data-testid="pricing-section"]').scrollIntoView();
cy.get('[data-testid="pricing-section"]').should('be.visible');
cy.document().then((doc) => {
const images = [...doc.images];
return Cypress.Promise.all(images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
});
cy.screenshot('full-page', { capture: 'fullPage' });
The image wait above lets failed images settle too; add a separate assertion if particular images must load successfully. Cypress recommends deliberate snapshots and notes that element-level diffs can reduce unrelated failures. For available capture modes and options, see the Cypress screenshot API, screenshot command, and visual testing guidance.
Puppeteer
With Puppeteer, trigger the content and readiness condition before capturing. The example scrolls in steps, waits for images to settle, and captures the full page.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(async () => {
const step = Math.max(1, window.innerHeight - 100);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
await page.waitForFunction(() =>
[...document.images].every(img => img.complete)
);
await page.waitForSelector('[data-testid="pricing-section"]', { visible: true });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
For a component-only contract, target the element instead of capturing the entire page:
const section = await page.$('[data-testid="pricing-section"]');
if (!section) throw new Error('Pricing section was not found');
await section.screenshot({ path: 'pricing-section.png' });
See the Puppeteer screenshots guide and ScreenshotOptions reference.
6. Choose the right comparison surface
| Situation | Recommended capture | Reason |
|---|---|---|
| A page-wide layout change could break users | Full page | It covers the page’s scrollable layout |
| A single component has a visual contract | Element or clipped region | It reduces noise from unrelated page areas |
| Below-fold content appears after scroll or click | Trigger behavior, wait for readiness, then capture | Capture extent alone does not load content |
| Sticky controls repeat in a stitched image | Element capture or controlled screenshot styling | Stitching can duplicate fixed-position content |
For a service-managed workflow, compare browser and viewport coverage, baseline ownership, approval flow, data handling, integrations, and current pricing directly with vendors. Cypress discusses open-source visual testing and commercial integrations in its visual testing guidance; current terms need their own verification.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the first screen appears | Viewport capture is in use, or fullPage is false |
Enable the framework’s full-page mode and inspect the resulting artifact. |
| The image is full height, but lower images are blank | Lazy images had not entered the loading region before capture | Scroll through the relevant sections, wait for required images, then capture. |
| A section is absent even after scrolling | It requires a click, a nested-container scroll, data, or another state transition | Reproduce the actual user interaction and wait for a page-specific readiness signal. |
| The test times out waiting for all images | An optional or broken image never reaches the assumed success condition | Wait only for required images or use a section-level application condition; assert required image success separately. |
| Sticky navigation appears multiple times | Cypress full-page capture stitches scroll captures | Compare a targeted region or temporarily adjust positioning in a controlled screenshot hook. |
| Diffs change between identical runs | Data, animations, fonts, ads, timing, viewport, or rendering environment varies | Control fixtures and environment, wait for app readiness, and mask only a small dynamic region when justified. |
| Screenshot is stable but still shows stale content | Stability was mistaken for application readiness | Wait for the expected data or state before invoking screenshot stabilization. |
| Cypress artifact includes the command log | The capture is in runner mode | Use the application capture mode intended for the comparison. |
8. Performance, reliability, and cost
Full-page screenshots take longer and produce larger images than viewport or element captures. Scrolling every section and waiting for media adds runtime too, so trigger only the regions covered by the test and prefer targeted snapshots when they express the actual contract. Infinite-scroll pages need a defined stopping condition; otherwise, the test may never reach a stable end.
For reliability, avoid using network-idle alone as proof that a page is ready: analytics, long polling, or background requests can keep activity going, while application content may be ready before all network traffic stops. Pair navigation with a concrete page condition. Keep timeouts bounded and report which readiness condition failed so a capture failure is diagnosable.
For cost, local Cypress, Playwright, and Puppeteer capture shifts the work of browser execution, baseline storage, CI artifacts, and review to your team. Managed visual testing may cover some of that workflow; compare current vendor terms and the operational cost of maintaining it. No service price or performance benchmark is assumed here.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It can capture a URL as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options, including full-page capture and waiting for page readiness.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents use screenshot tools. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up free and get 1,000 screenshots a month with no card.
FAQ
Why does my screenshot only capture above the fold?
The monitor may be taking a viewport screenshot. Enable full-page capture, then verify the saved artifact includes the lower page.
Does full-page capture automatically load lazy images?
No. Trigger the relevant scroll or interaction and wait for the images or application state your comparison requires.
Should every visual test use a full-page image?
No. Use full-page coverage for page-wide contracts and element or clipped captures for component-level contracts.
Will waiting for two matching screenshots fix missing content?
It can reduce transient differences, but it does not trigger lazy loading or ensure that the intended application state has appeared.


