ScreenshotNeo

BlogHow-to

How to Build Website Screenshot Functionality with Code

Build website screenshots with Playwright or Puppeteer. Capture a viewport, full page, or element, handle readiness and output, and troubleshoot common failures.

By the ScreenshotNeo team4 October 202610 min read

To build website screenshot functionality, open the target page in an automated browser, wait until the content you need is ready, choose whether to capture the viewport, the full page, or one element, then save the image or pass its bytes to your application. Playwright supports page and locator screenshots; Puppeteer supports page and element screenshots. The examples below use JavaScript, with Python and cURL options where they apply.

1. Choose the capture approach

For application code, use a browser automation library. Playwright has JavaScript and Python bindings and supports viewport, full-page, and locator screenshots. Puppeteer offers a JavaScript API for browser automation and page or element capture. For a lower-level Chromium integration, the Chrome DevTools Protocol exposes Page.captureScreenshot; most applications can start with a framework API.

Requirement Approach Considerations
Visible viewport page.screenshot() Captures the current browser viewport. Content below the fold is excluded.
Entire page Playwright fullPage: true Produces a tall image; check whether the destination can handle its dimensions and file size.
One component Playwright locator screenshot or Puppeteer element screenshot Useful for cards, charts, or headers. Puppeteer attempts to scroll an element into view if needed.
Further processing Return bytes or a buffer Use bytes for storage, transformation, or an HTTP response. Base64 is an optional encoding, not a different capture mode.

Set the viewport before navigation when responsive layout matters. A page may respond differently if its dimensions change after it has loaded. CSS viewport dimensions and device scale factor also affect the resulting pixel dimensions.

2. Capture screenshots with Playwright in JavaScript

Install Playwright and its browser once in the project environment:

npm install playwright
npx playwright install chromium

Save this as screenshot.mjs. It accepts a URL, captures the full page by default, and can capture a CSS-selected element with --selector. Use --viewport for a viewport-only image.

import { chromium } from 'playwright';

const target = process.argv[2] ?? 'https://example.com';
const selectorFlag = process.argv.indexOf('--selector');
const selector = selectorFlag >= 0 ? process.argv[selectorFlag + 1] : undefined;
const viewportOnly = process.argv.includes('--viewport');

const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1
  });
  await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 });

  // Replace or supplement this with an application-specific readiness signal
  // when content renders asynchronously.
  if (selector) {
    const locator = page.locator(selector);
    await locator.waitFor({ state: 'visible', timeout: 10000 });
    await locator.screenshot({ path: 'element.png' });
  } else {
    await page.screenshot({
      path: 'screenshot.png',
      fullPage: !viewportOnly,
      animations: 'disabled'
    });
  }
} finally {
  await browser.close();
}

Run it with:

node screenshot.mjs https://example.com
node screenshot.mjs https://example.com --viewport
node screenshot.mjs https://example.com --selector .header

Playwright’s screenshot API also returns image bytes when called without a path. Store the result, send it in a response, or hand it to an image-processing library:

const pngBytes = await page.screenshot({ fullPage: true });
// Example: pass pngBytes to your storage or image-processing code.

3. Capture with Playwright in Python

Install the Python package and Chromium browser:

python -m pip install playwright
python -m playwright install chromium

Save as screenshot.py and run python screenshot.py https://example.com. Pass --viewport for viewport capture or --selector .header for an element.

import asyncio
import sys
from playwright.async_api import async_playwright

async def main():
    target = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
    viewport_only = "--viewport" in sys.argv
    selector = None
    if "--selector" in sys.argv:
        selector = sys.argv[sys.argv.index("--selector") + 1]

    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        try:
            page = await browser.new_page(
                viewport={"width": 1440, "height": 1000},
                device_scale_factor=1,
            )
            await page.goto(target, wait_until="domcontentloaded", timeout=30000)
            if selector:
                element = page.locator(selector)
                await element.wait_for(state="visible", timeout=10000)
                await element.screenshot(path="element.png")
            else:
                await page.screenshot(
                    path="screenshot.png",
                    full_page=not viewport_only,
                    animations="disabled",
                )
        finally:
            await browser.close()

asyncio.run(main())

For an asynchronous application, wait for a selector or another state that represents the content your screenshot needs. A document reaching domcontentloaded does not guarantee that client-rendered content is ready.

4. Capture with Puppeteer in JavaScript

Install Puppeteer, which downloads a compatible browser as part of its normal installation:

npm install puppeteer

This script demonstrates a page capture, full-page option, and element capture. Save it as puppeteer-shot.mjs.

import puppeteer from 'puppeteer';

const target = process.argv[2] ?? 'https://example.com';
const selectorFlag = process.argv.indexOf('--selector');
const selector = selectorFlag >= 0 ? process.argv[selectorFlag + 1] : undefined;
const viewportOnly = process.argv.includes('--viewport');

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
  await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 });

  if (selector) {
    const element = await page.waitForSelector(selector, { visible: true, timeout: 10000 });
    await element.screenshot({ path: 'element.png' });
  } else {
    await page.screenshot({ path: 'screenshot.png', fullPage: !viewportOnly });
  }
} finally {
  await browser.close();
}

Run node puppeteer-shot.mjs https://example.com, optionally adding --viewport or --selector .header. Puppeteer’s documented example uses networkidle2 as a navigation condition. Treat that as one possible signal: pages with persistent requests or delayed application rendering may need a different condition or an explicit readiness check.

5. Choose readiness and capture scope

Wait for the right state

Navigation completion and visual readiness are different. Common choices include:

  • domcontentloaded: the initial HTML has been parsed; client rendering and images may still be in progress.
  • load: load-event resources have completed, but application data or later content may still be pending.
  • networkidle or Puppeteer’s networkidle2: useful on some pages, but unsuitable when requests stay open or continue in the background.
  • An application-specific signal: wait for a known element to appear, become visible, or reach a meaningful state. This is usually clearest when your application controls the target.

For a page you control, add a stable readiness marker and wait for it before taking the screenshot. For a third-party page, choose a bounded navigation wait and then wait for the content you actually need. Avoid replacing a readiness condition with an arbitrarily long fixed sleep: it wastes time on fast pages and can still miss slow ones.

Viewport, full page, and element

  • Viewport: use when you need what a visitor sees without scrolling. Set width and height before navigation.
  • Full page: use for a long-page record or review. The image can be very tall, consume more memory, and exceed limits in downstream image viewers or storage. Full-page capture is not the same as a sequence of viewport screenshots.
  • Element: use a locator or element handle for a specific component. Ensure it exists and is visible; a locator can match zero or multiple elements, and a detached or changing element can fail during capture.

For responsive comparisons, capture at the intended CSS viewport and set device scale factor deliberately. A scale factor above one creates more output pixels and larger files; it does not change the CSS layout width in the same way as changing the viewport.

6. Output formats and delivery

Playwright and Puppeteer screenshot APIs can save to a path or return bytes. Keep bytes in memory when you need to return the image from an endpoint or pass it to another service. Write to a path for local artifacts or a storage upload workflow. Base64 can be useful in text-only transport formats, but it increases the encoded payload size; prefer binary responses where available.

Choose an image format based on the consumer and fidelity needs. PNG is a common lossless choice for UI details. JPEG is useful when lossy compression is acceptable, and WebP may reduce size where the destination supports it. Browser API format options vary, so check the framework API for the installed version before relying on a particular format or quality setting. The examples use PNG.

For reproducible output, control the viewport, device scale factor, color scheme, fonts, locale, timezone, and animation state as needed. Dynamic data, remote fonts, ads, and third-party widgets can still cause differences between runs. If the screenshot is consumed by software, record the target URL and capture settings alongside the image.

7. cURL for a screenshot API

cURL does not render a web page by itself. It can call a screenshot service that runs the browser capture for you. With ScreenshotNeo, the following request saves a WebP image:

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

See the ScreenshotNeo API documentation for request options. For a local browser implementation, use one of the Playwright or Puppeteer examples above.

8. Reliability, performance, and cost

Keep captures bounded and repeatable

  • Set navigation, selector, and overall job timeouts so one stalled site cannot hold a worker forever.
  • Always close a browser or page when a one-shot job finishes. In a service, reuse browser processes carefully and isolate pages or contexts between jobs so cookies and state do not leak across users.
  • Limit concurrency based on available memory and CPU. Full-page images and high device scale factors increase resource use.
  • Use a predictable readiness signal and fixed capture settings when comparing screenshots over time.
  • Handle failed navigation, missing selectors, and empty output as explicit errors. Retrying every failure immediately can amplify load; use bounded retries for transient failures.

Understand the costs

Self-hosting means operating the browser runtime and allocating compute and storage; actual cost depends on workload and infrastructure, and the cited browser documentation does not provide a universal benchmark. A hosted screenshot API trades browser operations for per-plan usage. ScreenshotNeo offers 1,000 shots per month free with no card; paid plans are 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, and every feature is on every plan. See the product site for current details.

9. Troubleshooting

Symptom Likely cause Fix
Screenshot is blank or missing app content The page shell loaded before client-side content was ready. Wait for an application-specific selector or readiness marker before capture.
Navigation times out on an otherwise usable page Persistent analytics, streaming, or long-polling requests prevent the selected network-idle condition. Use a different bounded navigation condition, then wait for the content selector you need.
Element screenshot reports no element or times out The selector is wrong, the element is conditional, or it is not visible. Check the selector in the rendered page, wait for the intended state, and confirm it matches the expected element.
Full-page image is huge or fails downstream The page is exceptionally long or the consumer has pixel or payload limits. Capture a specific element, use viewport shots, or process the image in chunks appropriate to the receiving system.
Images or fonts are absent Resources load later, fail remotely, or are blocked in the runtime. Wait for the needed resources or app signal; check network access and resource errors in the browser environment.
Layout differs between runs Viewport, scale factor, fonts, locale, animation, or dynamic page data changed. Fix capture settings and disable animations where supported; stabilize or mock changing data for pages you control.
Browser fails to launch in a container The runtime lacks browser dependencies or has restrictive process settings. Install the automation framework’s browser and required dependencies for that environment; check its official installation instructions.

10. Or skip the browser setup

If your goal is to add screenshot capture to an application without installing and operating a browser, ScreenshotNeo accepts one GET request with a URL and returns a PNG, JPEG, WebP, or PDF. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

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)
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())));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. AI agents can capture through the MCP server, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. The API supports full-page and element capture, custom CSS and JavaScript, viewport and device settings, waits, headers and cookies, caching, async jobs, and bulk capture. Read the API documentation, then sign up for 1,000 free screenshots a month with no card.

FAQ

Can I return the screenshot directly from a web endpoint?

Yes. Capture to bytes and send those bytes with the correct image content type, or upload the file to your storage layer and return its reference. Avoid base64 unless your transport requires text.

Does a full-page screenshot capture infinite-scroll content?

Not necessarily. Full-page capture covers the document’s rendered extent at capture time. Infinite-scroll pages may load more content only as the page is scrolled, so scroll and wait for content deliberately before capture if you need it.

Should I use Playwright or Puppeteer?

Choose the library that fits your language and existing browser automation setup. Both document page screenshots; Playwright also documents locator screenshots, while Puppeteer documents element screenshots.

References