How to Take Full-Page Screenshots with ChromeDriver in Headless Mode
Use ChromeDriver to capture an entire webpage as a PNG with Chrome DevTools Protocol, then troubleshoot rendering, sizing, and save errors.
To capture a full webpage with ChromeDriver in headless mode, start Chrome through ChromeDriver, navigate to the page, wait for the content you need to render, and call the Chrome DevTools Protocol (CDP) command Page.captureScreenshot with captureBeyondViewport: true. Decode the returned base64 data and write it to a PNG file. A normal viewport screenshot only captures the visible area; CDP has an explicit option for content beyond it.
This guide uses Python with Selenium 4. The key capture command is CDP, while ChromeDriver creates and configures the Chrome session. Keep Chrome, ChromeDriver, and Selenium compatible; CDP bindings and behavior are versioned with the browser. See the CDP Page.captureScreenshot reference, Chrome Headless documentation, and ChromeDriver capabilities documentation.
1. Install Selenium and prepare Chrome
Install Selenium for Python:
python -m pip install selenium
Make a compatible Chrome browser available in your environment. Selenium can manage the driver in supported setups; otherwise provide a ChromeDriver executable compatible with your installed Chrome. The example below uses Selenium’s standard Chrome service discovery.
Choose a viewport width deliberately. It determines responsive breakpoints, text wrapping, and page layout, even when the image extends below the viewport. For example, use 1440 by 1000 for a desktop layout or 412 by 892 for a narrow mobile layout. These are examples, not universal defaults.
2. Capture the full page as PNG
Save as full_page.py and run python full_page.py https://example.com. The script captures the document dimensions, requests a beyond-viewport screenshot, decodes the base64 payload, and writes full-page.png.
import base64
import sys
from urllib.parse import urlparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
def main():
if len(sys.argv) != 2:
raise SystemExit("Usage: python full_page.py https://example.com")
url = sys.argv[1]
parsed = urlparse(url)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise SystemExit("Provide a complete http:// or https:// URL")
options = Options()
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(60)
driver.get(url)
# This only waits for document readiness. For applications that render
# content asynchronously, replace or extend it with a site-specific wait.
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
metrics = driver.execute_cdp_cmd("Page.getLayoutMetrics", {})
size = metrics.get("cssContentSize") or metrics["contentSize"]
width = int(size["width"])
height = int(size["height"])
if width < 1 or height < 1:
raise RuntimeError(f"Invalid document dimensions: {width}x{height}")
result = driver.execute_cdp_cmd(
"Page.captureScreenshot",
{
"format": "png",
"captureBeyondViewport": True,
"captureBeyondViewport": True,
"clip": {
"x": 0,
"y": 0,
"width": width,
"height": height,
"scale": 1,
},
"fromSurface": True,
},
)
image_bytes = base64.b64decode(result["data"], validate=True)
with open("full-page.png", "wb") as image_file:
image_file.write(image_bytes)
print(f"Saved full-page.png ({width}x{height} CSS pixels)")
finally:
driver.quit()
if __name__ == "__main__":
main()
The sample requests a clip spanning the layout dimensions and enables captureBeyondViewport. CDP defines the latter as false by default, so set it explicitly. Some browser and client combinations can capture beyond the viewport without an explicit clip; retrieving layout metrics makes the intended document bounds clear. If the page is exceptionally long or wide, inspect the output and consider a tiled capture strategy or PDF, because a single raster image can require substantial memory.
The --headless argument is the Chrome startup argument used in the example. ChromeDriver passes Chrome arguments through ChromeOptions. Match the argument to the Chrome release in your environment, especially if maintaining older installations.
3. Wait for the page state you need
driver.get() returning and document.readyState == "complete" do not guarantee every image, font, asynchronous widget, or lazy-loaded section is visible. Wait for an application-specific signal where possible, such as a result container or loading indicator disappearing:
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "main .report"))
)
WebDriverWait(driver, 30).until(
EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner"))
)
If the page uses lazy loading triggered by scrolling, scroll through it before measuring dimensions and capturing. This helper advances by roughly one viewport at a time, pauses briefly for content to load, and returns to the top. Tune the delay for the site; a fixed pause is only a heuristic.
import time
def trigger_lazy_content(driver, pause=0.25):
total_height = driver.execute_script(
"return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
)
viewport_height = driver.execute_script("return window.innerHeight")
y = 0
while y < total_height:
driver.execute_script("window.scrollTo(0, arguments[0])", y)
time.sleep(pause)
y += max(viewport_height - 100, 1)
driver.execute_script("window.scrollTo(0, 0)")
time.sleep(pause)
Call trigger_lazy_content(driver) after the page readiness waits and before Page.getLayoutMetrics. Scrolling can change document height as new content arrives, so the helper uses the initial height; for pages that keep extending, repeat the measurement and scroll pass until height stabilizes or a bounded time limit is reached.
4. Configure output format and viewport
Page.captureScreenshot supports PNG, JPEG, and WebP. PNG is lossless and a sensible default for text, diagrams, and general page captures. JPEG is lossy and may be smaller for photographic pages; provide the quality parameter for JPEG. WebP is supported by the protocol, but verify that downstream tools accept it. The command returns base64 data regardless of format.
result = driver.execute_cdp_cmd(
"Page.captureScreenshot",
{
"format": "jpeg",
"quality": 85,
"captureBeyondViewport": True,
"fromSurface": True,
},
)
with open("full-page.jpg", "wb") as f:
f.write(base64.b64decode(result["data"], validate=True))
For WebP, use "format": "webp" and save the decoded bytes with a .webp extension. The protocol reference documents the available parameters and return payload; check it against the CDP version exposed by the Chrome session.
- Viewport: set
--window-size=WIDTH,HEIGHTto choose responsive layout. Width is especially important for page composition. - Clip: pass an explicit
clipwith x, y, width, height, and scale when you need defined bounds. Use document layout metrics for a full-document clip. - Beyond viewport: set
captureBeyondViewportto true for content outside the visible viewport. - Format: choose PNG, JPEG, or WebP. JPEG supports quality in the protocol.
- Background: CDP has an
omitBackgroundoption for transparency where supported by the chosen format and browser. PNG is generally the appropriate format to preserve transparency. - Surface capture:
fromSurfaceselects surface capture behavior; the sample sets it true.
5. Why Chrome’s screenshot flag may not capture the whole page
Chrome Headless documents --screenshot as saving a screenshot of the target page and documents --window-size as a useful companion. That CLI example is useful for a screenshot workflow, but it does not document a general guarantee that arbitrary document content below the viewport will be included. For automation, CDP’s explicit captureBeyondViewport parameter is the relevant control.
Chrome Headless also supports --timeout to limit how long the CLI waits before capture. A timeout is a ceiling, not proof that a site’s asynchronous data, lazy images, or animations have settled.
6. PDF alternative for long pages
If the goal is a readable document rather than one tall raster image, Chrome Headless can print to PDF. This is a different output path: print styles and pagination may change how the page appears.
chrome --headless --print-to-pdf=page.pdf --no-pdf-header-footer https://example.com
Chrome’s current documentation uses --no-pdf-header-footer to omit print headers and footers. Older versions may use --print-to-pdf-no-header. Check the flags supported by the installed Chrome release.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Image shows only the first screen | The capture used the visible viewport, or captureBeyondViewport was omitted and defaults to false. |
Use CDP Page.captureScreenshot with captureBeyondViewport: true. Check that the clip covers the document dimensions. |
| Bottom sections or images are blank | Lazy content did not load, or asynchronous rendering had not finished. | Wait for meaningful page conditions, scroll through lazy content, then remeasure dimensions and capture. |
| Layout is unexpectedly narrow or rearranged | The configured viewport triggered a different responsive breakpoint. | Set an intentional width with --window-size and verify the rendered viewport before capture. |
| ChromeDriver cannot start Chrome | Chrome and ChromeDriver are incompatible, Chrome is missing, or the environment cannot launch a browser. | Install a compatible browser and driver, inspect the startup error, and use the ChromeOptions argument accepted by that release. |
| CDP command or parameter is rejected | The Selenium binding or browser protocol version differs from the example’s available protocol. | Check the CDP version for the active Chrome session and the installed Selenium binding. Keep browser and driver aligned. |
| Screenshot is cut off or capture fails on a very long page | The requested raster dimensions may exceed practical memory, graphics, or image-consumer limits. | Reduce the captured region, capture sections and stitch them, or print to PDF. There is no universal maximum height established here. |
| Python reports a base64 decoding error | The returned data was modified or not the expected CDP screenshot field. | Read result["data"] directly and avoid treating the base64 string as a data URL. |
| Output file is zero bytes or will not open | Capture failed before valid image data was saved, or the wrong extension/format was used. | Check for CDP exceptions, verify the result contains data, and match the extension to the requested format. |
8. Performance, reliability, and cost considerations
Full-page capture is sensitive to document dimensions: taller pages produce more pixels, more encoded bytes, and greater memory demand. Use a viewport width only as large as the layout requires, wait for the page state you need rather than sleeping for an unnecessarily long fixed interval, and avoid capturing huge pages as one raster if the consumer does not need it. For recurring captures, bound navigation and wait times, always call driver.quit() in a finally block, and record failures separately from valid images.
Reliability depends on the target page as well as the browser setup. Authentication, bot checks, network errors, changing content, animation, and third-party widgets can alter what appears. Stabilize the intended state with explicit waits and use the same viewport and browser configuration across runs. No universal timing, height limit, or success rate applies to every page.
Running ChromeDriver means maintaining a browser runtime, compatible driver, execution environment, and any retry or storage logic. A managed screenshot API trades that setup for per-plan usage; compare the needed capture controls and billing behavior before choosing. The next section gives a one-request option.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before taking the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents.
Start with the ScreenshotNeo API documentation. This cURL example saves a WebP capture:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.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://example.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 includes full-page capture with lazy images loaded, element capture by CSS selector, dark mode, device presets and custom viewports, retina scale, PDF options, custom CSS and JavaScript, click-before-capture, selector hiding and waits, request and resource blocking, custom headers and cookies, user agent and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed public image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI spec. Parameter names used by other screenshot APIs also work.
Every feature is on every plan: 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Higher tiers are Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Can Selenium take a full-page screenshot without CDP?
Selenium’s ordinary screenshot methods capture the current viewport. For this Chrome-based workflow, use CDP’s beyond-viewport capture option to request the full document.
Does headless mode change the page layout?
The viewport and browser environment can affect responsive layout and rendering. Set the intended window size and inspect the result rather than assuming it matches another browser session exactly.
Can I capture only one element?
CDP screenshot clipping uses coordinates and dimensions. For a specific element, locate its bounding rectangle and use those bounds as a clip, or use a screenshot API with selector-based element capture.
Should I use PNG or PDF?
Use PNG for a single raster image. Use PDF when pagination and print styling are acceptable and a document is more useful than a very tall image.


