How to Capture a Scrolling Parallax Website Screenshot
Capture a parallax page as one tall image or record its changing scroll states with Playwright. Includes runnable code, troubleshooting, and an API option.
A full-page screenshot gives you one tall image of a page; it does not necessarily show every visual state of a scrolling parallax effect. Use Playwright’s fullPage: true for a page overview. If the important evidence is how layers move or content appears during scrolling, scroll to selected positions and capture separate viewport or section images.
This guide uses Node.js and Playwright. The same choice applies whether you need a design review, a visual record, or screenshots in an automated workflow: decide whether you need the whole document or specific scroll states, then inspect the output.
1. Choose what the screenshot needs to show
| Capture | Best for | Trade-off |
|---|---|---|
| Full page | A single overview of the document | One tall image may not communicate multiple scroll-dependent parallax states. |
| Viewport at chosen positions | Showing what a visitor sees at particular scroll offsets | Produces several images; you choose the positions. |
| Element or section | Isolating one part of the design | Does not document the entire page. |
Playwright documents full-page screenshots as capturing the full scrollable page as if it fit on a very tall screen. That is useful for an overview, but a page whose artwork changes with scroll position may need multiple captures to record those states. This is an inference from the capture model and parallax behavior, not a guarantee about how any particular site renders.
2. Set up a repeatable Playwright capture
Install Node.js, create a project, and install Playwright. The first command initializes a package manifest; the second installs Playwright and its browser.
npm init -y
npm install playwright
npx playwright install chromium
Save the following as capture-parallax.mjs. Set TARGET_URL to the page you are allowed to capture. The script records one full-page image, then scrolls through the document and captures viewport images at evenly spaced positions. It waits for fonts and a short settling interval, but those waits are practical defaults, not a universal guarantee that animations or lazy content have finished.
import { chromium } from 'playwright';
const targetUrl = process.env.TARGET_URL;
if (!targetUrl) throw new Error('Set TARGET_URL to the page URL');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
try {
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 60_000 });
await page.evaluate(() => document.fonts.ready);
// Give scroll-triggered effects and late layout changes a moment to settle.
await page.waitForTimeout(500);
// One tall overview image.
await page.screenshot({ path: 'full-page.png', fullPage: true });
// Scroll in viewport-sized increments so content near the bottom is reached.
const viewportHeight = page.viewportSize().height;
const pageHeight = await page.evaluate(() => document.documentElement.scrollHeight);
const maxScroll = Math.max(0, pageHeight - viewportHeight);
const step = Math.max(1, Math.floor(viewportHeight * 0.8));
let index = 0;
for (let y = 0; y <= maxScroll; y += step) {
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
await page.waitForTimeout(400);
await page.screenshot({ path: `state-${String(index).padStart(3, '0')}.png` });
index += 1;
}
// Ensure the exact bottom position is included if the step did not land on it.
if (maxScroll % step !== 0) {
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), maxScroll);
await page.waitForTimeout(400);
await page.screenshot({ path: `state-${String(index).padStart(3, '0')}.png` });
}
} finally {
await browser.close();
}
Run it with:
TARGET_URL='https://example.com' node capture-parallax.mjs
The screenshots are written to the current directory. For a repeatable comparison, keep the browser, viewport, device scale factor, URL, and capture positions consistent. If your site requires authentication, create a Playwright browser context with the appropriate storage state or log in through the page before capturing; do not put credentials directly in source control.
3. Capture only the states that matter
Evenly spaced captures are a starting point. For review, choose positions around section boundaries, pinned elements, and transitions where the foreground and background visibly shift. You can replace the loop with explicit scroll offsets:
const positions = [0, 650, 1320, 2100];
for (const [index, y] of positions.entries()) {
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
await page.waitForTimeout(400);
await page.screenshot({ path: `selected-${index}.png` });
}
To capture a particular section, locate it and use an element screenshot. This is helpful when a section is the evidence you need; it does not replace viewport captures when the page’s effect depends on the surrounding scroll state.
const section = page.locator('#parallax-section');
await section.waitFor({ state: 'visible' });
await section.screenshot({ path: 'parallax-section.png' });
Replace #parallax-section with a selector from the target page. If the element is covered, outside the viewport, or not present yet, adjust the selector or wait for the site’s content to load. Review the result for clipped edges and overlays.
4. Make lazy content and animation timing predictable
Some pages load images or sections only as they approach the viewport. Scroll through the page before taking the full-page image, then return to the top if you want a particular initial state. For example, insert this before the full-page screenshot:
await page.evaluate(async () => {
const step = Math.max(300, Math.floor(window.innerHeight * 0.75));
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise((resolve) => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
});
await page.waitForTimeout(500);
This is a simple approach, not a universal lazy-load detector. A site may use custom scroll listeners, intersection observers, long-running animations, or content that loads after a network response. Increase the settling time or wait for a known selector when you know the page’s behavior. Avoid assuming that waiting for network idle proves all visual changes have completed.
When comparing design changes, consider disabling animation through page CSS only if the purpose is a stable layout comparison. If the purpose is to document the parallax effect, leave the animation enabled and capture the selected scroll states. Keep those two goals separate in your capture process.
5. Inspect the images before relying on them
- Check that lazy images and lower-page sections appear.
- Look for blank areas, repeated content, or layout shifts between captures.
- Check sticky and fixed elements: they may appear in each viewport image, while their placement in a tall image may not match a visitor’s experience.
- Confirm each chosen scroll state shows the intended parallax composition.
- Compare at the same viewport and device scale when the screenshots are evidence for a visual regression.
A full-page image answers “what does the document look like as one tall image?” A set of viewport captures answers “what does the visitor see at these scroll positions?” Use the one that matches the question being reviewed.
6. cURL, Python, and Node.js alternatives
Playwright is the do-it-yourself option in this guide. cURL and Python can automate the same browser workflow by invoking Playwright code, but the simplest reliable arrangement is to keep the browser capture in a Node.js script and call it from your existing job runner. If you need a single HTTP request instead of managing a browser, see the ScreenshotNeo option below.
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A single GET request returns an image or PDF, and its documented options include full-page capture, CSS selector capture, custom CSS and JavaScript, wait conditions, and viewport/device settings. A single full-page request still represents one page capture; when you need distinct parallax scroll states, capture selected states with a browser workflow.
For a standard full-page shot, use the API code below. See the ScreenshotNeo API documentation for parameters and configuration.
cURL
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 Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
The Node example uses Bun’s file-writing API. In Node.js, save the response with the built-in filesystem module:
import { writeFile } from 'node:fs/promises';
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 writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; higher plans are 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, and every feature is available on every plan. Sign up for 1,000 free screenshots a month, with no card required.
8. Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
| The full-page image does not show the effect seen while scrolling. | A tall image captures the document layout, not every scroll-dependent state. | Capture viewport images at the positions where the effect changes. |
| Images or sections near the bottom are blank or missing. | Content may load only when it approaches the viewport. | Scroll through the page first, wait for known content, then capture and inspect again. |
| The screenshot shows a transition midway. | The capture happened while an animation or scroll-triggered transition was in progress. | Wait after each scroll; capture stable states or choose a position away from the transition. |
| The script times out at navigation. | The page may keep network connections open, load slowly, or fail to settle before the timeout. | Check the URL and access in a browser. If appropriate for the site, use a different navigation wait condition and explicitly wait for the content you need. |
| A section selector times out or matches nothing. | The selector may be wrong, or the section has not rendered. | Inspect the page’s DOM, use a stable selector, and wait for the relevant element. |
| Images differ between runs. | Viewport, device scale, timing, page content, or animation state may differ. | Fix the viewport and device scale, use consistent scroll offsets, and wait for fonts and page content. |
| A sticky item appears in unexpected places. | Sticky and fixed positioning depends on the viewport and scroll position. | Use viewport captures for visitor states; inspect the full-page result separately as a document overview. |
9. Performance, reliability, and cost
Local Playwright captures require a browser process and time for navigation, page loading, scrolling, and screenshot encoding. Capturing many states produces more files and takes longer than one screenshot. Limit the positions to the states that answer your review question, reuse a browser process for batches, and close it in a finally block so failures do not leave it running.
Reliability depends on the page as well as the script: remote assets can fail, content can change, and scroll-driven effects can be timing-sensitive. Keep capture settings stable and inspect images before using them as evidence. For scheduled work, record the URL, viewport, and selected offsets alongside the output so a later run can reproduce the same setup.
A local Playwright run has no per-shot API charge, but it uses your compute and requires maintaining browser dependencies. An API replaces browser setup with a request and has plan limits and pricing. ScreenshotNeo’s free allowance is 1,000 shots per month; paid plans begin at $5 for 3,000. Use the API when its supported page capture matches your need; use a controlled scroll workflow when the deliverable must show multiple parallax states.
10. FAQ
How do I take a full-page screenshot of a scrolling website?
In Playwright, call page.screenshot({ path: 'screenshot.png', fullPage: true }). This captures the full scrollable page as one tall image.
How can I screenshot a parallax website without losing the scroll effect?
Capture separate viewport images at meaningful scroll offsets. A single full-page image may not represent each scroll-dependent state.
Why does my full-page screenshot look different from what I see while scrolling?
The page can reposition or reveal layers as the viewport scrolls. A tall image is an overview, while separate viewport captures record specific positions.
Should I use full-page or element screenshots?
Use full-page for a document overview, viewport screenshots for scroll states, and an element screenshot when one section is the focus.


