ScreenshotNeo

BlogHow-to

How to Capture Responsive Website Screenshots at Multiple Widths with Selenium

Resize a Selenium browser to a set of widths, wait for responsive content to settle, and save a clearly named screenshot at each size.

By the ScreenshotNeo team4 October 202610 min read

Use Selenium WebDriver to set a browser window size for each target width, wait for the page’s responsive content to settle, and save a screenshot with that width in its filename. In Python, the core calls are driver.set_window_size(width, height) and driver.save_screenshot(path). Check window.innerWidth in the browser when you need the CSS viewport width: a requested outer window size does not guarantee the same viewport size across browser and driver environments.

This guide uses Python for the full workflow, then shows equivalent Selenium examples in JavaScript and HTTP examples for ScreenshotNeo. Selenium’s official [window and tab documentation](https://www.selenium.dev/documentation/webdriver/interactions/windows/) and [Python WebDriver API reference](https://www.selenium.dev/selenium/docs/api/py/webdriver_remote/selenium.webdriver.remote.webdriver.html) document the relevant browser control and screenshot methods.

1. What you need and what the capture means

Install Selenium for Python and have Chrome available. Selenium Manager can help manage drivers when you create a browser driver with current Selenium releases; in managed or restricted environments, configure the browser and driver according to your environment. The examples assume a normal desktop browser session and a page you are allowed to access.

Each screenshot represents the page after the browser has been resized. It is useful for checking responsive breakpoints and producing a set of visual artifacts. It does not, by itself, reproduce every property of a physical phone: device pixel ratio, touch input, browser chrome, and mobile user-agent behavior are separate factors. Use browser-specific emulation when those properties are part of the requirement.

Need Approach
Several responsive widths Loop through widths and call set_window_size for each one.
Exact CSS viewport width Read window.innerWidth and record it with the artifact.
Only visible browser viewport Use the regular screenshot method and confirm behavior in the chosen browser binding.
Whole document Use a full-page capture method supported by the binding/browser, or capture and stitch deliberately; do not assume a viewport screenshot covers the full document.

2. Capture a page at multiple widths with Python

Save this as capture_widths.py. It navigates once, resizes the same browser, waits for fonts and images to settle, records the effective CSS viewport width, and saves a PNG per requested width. The settling script has a timeout so a font or image that never finishes cannot hang the whole capture indefinitely.

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

URL = "https://example.com"
WIDTHS = [375, 768, 1280]
HEIGHT = 900
OUTPUT = Path("screenshots")
OUTPUT.mkdir(parents=True, exist_ok=True)

options = webdriver.ChromeOptions()
# For a CI/container environment, uncomment the next line if required:
# options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
try:
    driver.set_page_load_timeout(45)
    driver.get(URL)

    for requested_width in WIDTHS:
        driver.set_window_size(requested_width, HEIGHT)

        # Wait until the requested CSS viewport is active. Window dimensions
        # can differ from the page viewport due to browser chrome or the host.
        WebDriverWait(driver, 10).until(
            lambda d: d.execute_script("return window.innerWidth") > 0
        )
        viewport = driver.execute_script(
            "return {width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio}"
        )

        # Give resize handlers a rendering frame, then wait for fonts and images.
        driver.execute_async_script("""
            const done = arguments[arguments.length - 1];
            const timeout = setTimeout(() => done(), 5000);
            const images = Array.from(document.images);
            Promise.all([
              document.fonts ? document.fonts.ready : Promise.resolve(),
              ...images.map(img => img.decode ? img.decode().catch(() => {}) : Promise.resolve())
            ]).then(() => {
              requestAnimationFrame(() => requestAnimationFrame(() => {
                clearTimeout(timeout);
                done();
              }));
            }).catch(() => {
              clearTimeout(timeout);
              done();
            });
        """)

        actual_width = driver.execute_script("return window.innerWidth")
        path = OUTPUT / f"page-{requested_width}px-viewport-{actual_width}px.png"
        driver.save_screenshot(str(path))
        print(f"saved {path} (CSS viewport {viewport['width']}x{viewport['height']}, DPR {viewport['dpr']})")
finally:
    driver.quit()

Run it with python capture_widths.py. Replace URL, WIDTHS, and HEIGHT with the page and dimensions you need. The code saves the current browser window as PNG files; it does not claim to create full-document screenshots.

Use a fresh navigation at each width when the page has stateful layout

The basic loop resizes one loaded page. That is efficient and works for typical responsive CSS. For pages that initialize components only during navigation, cache layout measurements, or behave differently after a resize, navigate again at each width and wait for the same application-specific ready condition before capture:

for width in WIDTHS:
    driver.set_window_size(width, HEIGHT)
    driver.get(URL)
    WebDriverWait(driver, 15).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )
    # Add a page-specific wait here, such as visibility of the main content.
    driver.save_screenshot(f"screenshots/page-{width}px.png")

Navigation’s complete state only indicates document loading has completed; a client-rendered app may still need a wait for a known element or state. Prefer a meaningful condition over an arbitrary long sleep.

3. Choose widths and verify the viewport

Use widths that answer a concrete layout question: a narrow phone layout, a tablet or intermediate layout, and a wide desktop layout are a useful starting set. To test a breakpoint, capture just below and just above that breakpoint. There is no universal correct width list because breakpoints are defined by the page being examined.

set_window_size(width, height) controls the browser window in pixels. Window decoration, headless mode, remote browser setup, and operating system can affect how that maps to the page’s CSS viewport. The script records innerWidth, innerHeight, and device pixel ratio to make the artifact’s conditions visible. If an exact CSS width is mandatory, check the measured value and adapt the setup or use the browser’s viewport emulation facilities for that browser.

  • Requested width: value passed to Selenium’s window sizing method.
  • CSS viewport width: window.innerWidth, which drives many responsive CSS media queries.
  • Image pixel width: can reflect device pixel ratio and capture implementation, so inspect the saved file when exact output dimensions matter.

4. Wait for responsive content before saving

Resizing may trigger media-query changes immediately, but a page can also run JavaScript resize handlers, lazy-load assets, swap images, or load fonts afterward. Selenium does not prescribe one universal settling delay. The example waits for image decoding and fonts, then gives rendering two animation frames. For a dynamic application, wait for the exact content that matters.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

# Example: wait for the page's main content after each resize/navigation.
WebDriverWait(driver, 15).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "main"))
)

For lazy-loaded images lower on a long page, scrolling may be necessary to trigger their loading before a full-page capture. A regular viewport screenshot only captures the current visible area; use the full-page functionality available in your browser/binding if the entire document is required, and verify its result rather than assuming the default capture scope.

5. Capture multiple URLs or save metadata

For a repeatable review, keep the URL, requested dimensions, measured viewport, browser configuration, and capture time alongside the images. This helps distinguish a layout change from a different environment. A small manifest can be written from the same loop:

import json
from datetime import datetime, timezone

manifest = []
# After each screenshot in the loop:
manifest.append({
    "url": URL,
    "requested_width": requested_width,
    "viewport_width": actual_width,
    "height": viewport["height"],
    "device_pixel_ratio": viewport["dpr"],
    "file": str(path),
    "captured_at": datetime.now(timezone.utc).isoformat(),
})
# After the loop:
(OUTPUT / "manifest.json").write_text(json.dumps(manifest, indent=2))

When capturing multiple pages, use separate output names that include a stable page identifier and width. Sanitize URL paths before using them in filenames, and avoid allowing arbitrary page input to write outside the intended output directory.

6. Equivalent Selenium example in JavaScript

The Selenium JavaScript binding exposes window sizing and screenshot capture through WebDriver. Install the package with npm install selenium-webdriver and provide a Chrome browser/driver configuration appropriate to your environment.

const { Builder } = require('selenium-webdriver');
const fs = require('node:fs/promises');

(async () => {
  const url = 'https://example.com';
  const widths = [375, 768, 1280];
  const height = 900;
  const driver = await new Builder().forBrowser('chrome').build();

  try {
    await driver.manage().setTimeouts({ pageLoad: 45000, script: 10000 });
    await driver.get(url);

    for (const width of widths) {
      await driver.manage().window().setRect({ width, height });
      const viewportWidth = await driver.executeScript('return window.innerWidth');
      const png = await driver.takeScreenshot(); // Base64 PNG data
      await fs.writeFile(`page-${width}px-viewport-${viewportWidth}px.png`, Buffer.from(png, 'base64'));
    }
  } finally {
    await driver.quit();
  }
})();

Selenium’s JavaScript API describes screenshot capture as best effort, with possible capture targets including the whole page, current window, visible portion of the current frame, or display containing the browser. Confirm the scope produced by the binding and browser you use. See the official [JavaScript WebDriver API](https://www.selenium.dev/selenium/docs/api/javascript/WebDriver.html).

7. ScreenshotNeo one-call option

If you need screenshots at several widths without maintaining a browser and driver, [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server. Its API accepts a URL and screenshot options; see the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/). Set the viewport width and height for each capture. This is a separate hosted capture workflow from the Selenium examples above.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -d width=375 -d height=900 -o shot-375.webp
import requests

for width in [375, 768, 1280]:
    r = requests.get(
        "https://api.screenshotneo.com/v1/shot",
        params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "width": width, "height": 900},
        timeout=90,
    )
    r.raise_for_status()
    open(f"shot-{width}.webp", "wb").write(r.content)
const widths = [375, 768, 1280];
for (const width of widths) {
  const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com', width: String(width), height: '900' });
  const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
  const bytes = Buffer.from(await res.arrayBuffer());
  await import('node:fs/promises').then(fs => fs.writeFile(`shot-${width}.webp`, bytes));
}

Replace YOUR_API_KEY with your key and change the URL and dimensions. The API supports PNG, JPEG, or WebP output. Its options also include full-page capture, device presets, custom CSS and JavaScript, wait conditions, and other capture controls.

8. Troubleshooting

Symptom Likely cause Fix
Saved image has the wrong width Outer browser window size differs from CSS viewport width. Record window.innerWidth; adjust the requested window size or use browser viewport emulation where exact CSS dimensions matter.
Layout looks like the previous width Resize-triggered code has not finished, or the page only initializes layout on navigation. Wait for an application-specific state; if needed, reload after resizing.
Images or fonts are missing Assets are still loading, lazy loading was not triggered, or the remote resource failed. Wait for font/image readiness, scroll to trigger lazy assets when appropriate, and inspect the page/network conditions.
Screenshot file is blank or the page is an error screen Navigation failed, the destination is unavailable, or the browser is blocked. Check the current URL and page state before saving; add a condition for the expected page content and handle failed captures explicitly.
Driver cannot start Browser/driver installation or environment configuration is incompatible. Use a supported browser installation and Selenium setup; configure the driver or remote WebDriver endpoint for the environment.
Capture only contains the visible viewport The selected screenshot method captures the current window rather than the whole document. Use a supported full-page capture path and verify the resulting dimensions and content.
HTTP API returns an error Missing/invalid API key, invalid parameters, or a page capture failure. Check key and URL, inspect the response status and headers, and consult the [ScreenshotNeo docs](https://screenshotneo.com/docs/).

9. Performance, reliability, and cost

A single browser session reused across widths avoids repeatedly starting the browser. The page may still need to recalculate layout and fetch assets at every size. Capture only the widths that answer your review question, avoid parallel sessions that exceed your machine or CI capacity, and save files incrementally so one late failure does not discard earlier captures.

Reliability depends on stable browser/driver versions, a consistent viewport setup, explicit waits for page-specific content, and deterministic input state such as authentication and cookies. Always quit the driver in a finally block. For visual comparison, keep the browser, operating environment, font availability, page state, and capture dimensions consistent.

Local Selenium has no per-screenshot API charge in this workflow, but it uses compute, browser maintenance, and engineering time. ScreenshotNeo has a free plan with 1,000 screenshots per month and no card; paid plans start at $5 for 3,000 screenshots. Only clean shots are billed, while bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. The response includes X-Page-Verdict and X-Billed headers so capture outcomes and billing can be inspected.

10. Or skip the browser setup

With ScreenshotNeo, send one request per width and save the returned image. Cookie banners, newsletter 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 tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

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

Repeat with the widths you need. Read the [API docs](https://screenshotneo.com/docs/) and [create a free account](https://screenshotneo.com/account/sign-up/) to get started.

11. FAQ

Can I capture a specific breakpoint without testing all widths?

Yes. Choose the exact width relevant to the breakpoint, then also capture just below or above it if you need to see where the layout changes.

Does resizing the browser emulate a phone?

It changes the browser window size, which is useful for responsive layout checks. It does not alone guarantee mobile device behavior such as device pixel ratio, touch interaction, or mobile user-agent behavior.

Does save_screenshot save JPEG?

The documented Python method saves a PNG screenshot. Convert the resulting image separately if another format is needed; ScreenshotNeo supports PNG, JPEG, and WebP output.

Can I capture many pages and widths?

Yes. Nest a URL loop around the width loop, use stable filenames, and isolate failures per page so one inaccessible URL does not stop the entire batch.