Written By Hanzala Saleem
Updated At July 14, 2026 | 9 min read
You're mid-way through a test run, a page fails an assertion, and you need visual proof of what the browser actually rendered. Or you're building a monitoring script and someone asks, "can we just grab a screenshot of this page every hour?" Either way, you land on the same tool: Selenium.
Selenium can capture screenshots in every language it supports, but the built-in methods only grab what's inside the current viewport unless you do extra work. This guide walks through working Selenium screenshot code in Python, Java, and C#, covers the full-page workarounds and the problems that trip people up (lazy loading, sticky headers, timing), and compares Selenium against a screenshot API for teams that need this at scale, including for AI agents and LLM pipelines that need a reliable screenshot API for AI agents rather than a browser cluster to babysit.

Selenium screenshot capture is the process of using Selenium WebDriver's built-in commands to save an image of whatever a browser has rendered at a given moment. Under the hood, WebDriver asks the browser (Chrome, Firefox, Edge) to encode the current viewport as a PNG and return it as base64 data, which your test script then writes to disk. It's part of the W3C WebDriver specification, so the core method works the same way across browsers, even if the full-page behavior doesn't.
Screenshots aren't just for pretty test reports. Teams rely on them for:
| Type | What it captures | Common use case |
|---|---|---|
| Viewport screenshot | Only what's visible in the current browser window | Quick debugging, fast test runs |
| Full-page screenshot | The entire scrollable page, top to bottom | Documentation, visual audits, compliance |
| Element screenshot | A single DOM element (a button, a modal, a card) | Component-level visual testing |
Python is the most common language for Selenium test suites, and WebDriver exposes screenshot methods directly on the driver object and on any WebElement.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com")
# Viewport screenshot
driver.save_screenshot("viewport.png")
# Element screenshot
element = driver.find_element(By.TAG_NAME, "h1")
element.screenshot("heading.png")
finally:
driver.quit()What this does: save_screenshot() is a convenience wrapper around get_screenshot_as_file() and returns True on success, False on an I/O error. It only captures the current viewport, so anything below the fold is cut off. The element screenshot call works the same way but crops to the target element's bounding box, which is handy for testing a single card, header, or button in isolation. Headless mode is set through --headless=new, the flag Chromium now recommends over the older --headless argument.
Java's Selenium bindings use the TakesScreenshot interface, which every driver implements.
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.io.File;
import java.nio.file.Files;
public class ScreenshotExample {
public static void main(String[] args) throws Exception {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--window-size=1920,1080");
WebDriver driver = new ChromeDriver(options);
try {
driver.get("https://example.com");
File screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
Files.copy(screenshot.toPath(),
new File("viewport.png").toPath());
} finally {
driver.quit();
}
}
}What this does: WebDriver itself doesn't declare a screenshot method, so you cast the driver to TakesScreenshot first. getScreenshotAs(OutputType.FILE) returns a temporary file that Selenium deletes once the JVM exits, which is why the code copies it to a permanent location right away with Files.copy. Swap OutputType.FILE for OutputType.BYTES if you want the raw PNG bytes instead, for example to upload straight to S3 without touching disk.
The .NET bindings mirror the Java API almost exactly, through the ITakesScreenshot interface.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
var options = new ChromeOptions();
options.AddArgument("--headless=new");
options.AddArgument("--window-size=1920,1080");
IWebDriver driver = new ChromeDriver(options);
try
{
driver.Navigate().GoToUrl("https://example.com");
ITakesScreenshot screenshotDriver = (ITakesScreenshot)driver;
Screenshot screenshot = screenshotDriver.GetScreenshot();
screenshot.SaveAsFile("viewport.png");
}
finally
{
driver.Quit();
}What this does: GetScreenshot() returns a Screenshot object holding the image as a base64 string and a byte array, and SaveAsFile() writes it straight to disk in the format you specify. Because IWebDriver doesn't expose this method directly, you cast to ITakesScreenshot, exactly like the Java example. Microsoft's .NET documentation covers the base language, while the Selenium C# API reference covers the driver-specific pieces.
| Language | Screenshot call | Returns |
|---|---|---|
| Python | driver.save_screenshot(path) | True / False |
| Java | ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE) | File (or byte[]) |
| C# | ((ITakesScreenshot) driver).GetScreenshot() | Screenshot object |
None of the three examples above capture more than the visible viewport, and that's on purpose. Selenium's native screenshot command was never designed for full-page capture.
The limitation: the WebDriver spec only defines a viewport screenshot. Firefox's GeckoDriver added get_full_page_screenshot_as_file() as a non-standard extension, so Firefox can do it in one call. Chrome and Edge, which run on Chromium, don't expose an equivalent method through plain Selenium.
Browser differences: because Firefox and Chromium diverge here, teams that need consistent full-page output across both browsers usually end up writing two different code paths, or falling back to the workaround below for both.
Common workarounds for Chromium browsers:
Each workaround adds code, adds a browser-version dependency, and still breaks on lazy-loaded images that only appear after the page has been scrolled past.
| Issue | Why it happens | Typical fix |
|---|---|---|
| Lazy loading | Images/content load only when scrolled into view | Scroll incrementally before capture, add wait time |
| Responsive layouts | Fixed window size doesn't match the target device | Set explicit window-size per breakpoint |
| Authentication | Page redirects to a login wall | Inject session cookies or auth headers before navigation |
| Timing | Screenshot fires before the DOM or JS finishes rendering | Use explicit waits (WebDriverWait) instead of sleep() |
| Animations | CSS transitions mid-motion at capture time | Add a short delay or disable animations via injected CSS |
| Sticky headers | Repeats in every scrolled slice during stitching | Hide the element with injected CSS before capture |
| High DPI displays | Screenshot looks blurry on retina screens | Set device pixel ratio explicitly in browser options |
| Dynamic content | Ads, timestamps, or carousels shift between runs | Block or hide the element, or accept minor diff tolerance |
| CAPTCHA | Bot-detection blocks the page entirely | Avoid triggering detection; some sites cannot be automated this way |

| Factor | Selenium (self-hosted) | Screenshot API |
|---|---|---|
| Setup | Install drivers, manage browser binaries | One HTTP request, no install |
| Infrastructure | You provision and patch servers | Fully managed |
| Rendering accuracy | Depends on installed browser version | Consistent Chromium version every time |
| JavaScript support | Full, but you configure waits yourself | Full, with built-in wait/lazy-load handling |
| Speed | Depends on your hardware and browser startup time | Optimized, request-response |
| Scalability | Manual scaling, queues, orchestration | Handles concurrency for you |
| Maintenance | Ongoing (Chrome updates, driver mismatches) | None on your side |
| Authentication | Cookie/header injection via code | Cookie/header parameters in the request |
| Output formats | PNG (native), others need extra libraries | PNG, JPG, WebP, PDF |
| Full-page support | Requires workarounds (see above) | Built-in parameter |
| Concurrency | Limited by your own machine or grid | Handled by the provider |
| Cost | Server and DevOps time | Usage-based pricing |
Neither approach is wrong. If you're already running a Selenium test suite for functional testing, adding a screenshot call costs nothing extra. If screenshots are the actual product, or you need them at volume, the calculation changes.
A few situations tend to push teams away from raw Selenium and toward an API:
ScreenshotAPI.net runs every capture inside a real, isolated Chromium instance, so JavaScript-heavy React, Vue, and Next.js pages render the same way they would in an actual browser. It handles full-page scrolling, ad and cookie-banner blocking, and lazy-loaded content automatically, which removes several of the workarounds listed earlier in this guide. It also supports scheduled captures, bulk screenshot jobs, and direct storage to your own S3, Google Cloud, or Wasabi bucket.
// One HTTP request instead of managing a headless browser
const token = "YOUR_API_KEY";
const targetUrl = encodeURIComponent("https://example.com");
const query =
`https://shot.screenshotapi.net/v3/screenshot` +
`?token=${token}&url=${targetUrl}` +
`&full_page=true` +
`&block_ads=true` +
`&no_cookie_banners=true` +
`&lazy_load=true` +
`&file_type=png` +
`&output=image`;
fetch(query)
.then((response) => response.arrayBuffer())
.then((buffer) => {
// save or upload the returned image buffer
});What this does: token and url are required parameters; everything else is optional configuration. full_page=true scrolls the entire page before capturing, so nothing below the fold is missed, which is the exact gap in native Selenium screenshots described earlier. block_ads and no_cookie_banners strip out ads and consent popups automatically. lazy_load=true tells the renderer to scroll and wait for lazy-loaded images before capturing. Setting output=json instead of output=image returns a URL to the hosted screenshot rather than the raw image bytes, which is useful when you want to store a reference instead of downloading the file immediately. The full parameter list is in the render screenshot documentation.
| Practice | Why it matters |
|---|---|
Use explicit waits, not sleep() | Fixed delays waste time or fire too early on slow connections |
| Set a consistent window size | Keeps screenshots comparable across test runs |
| Disable animations before capture | Avoids mid-transition, inconsistent frames |
| Hide dynamic elements (ads, timestamps) | Reduces false positives in visual diffs |
| Run headless in CI | Faster, and avoids requiring a display server |
| Version-pin your browser driver | Prevents silent breakage after a Chrome auto-update |
| Store screenshots with a timestamp in the filename | Makes audit trails and rollbacks easier to trace |
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or white screenshot | Page hadn't finished rendering | Add an explicit wait for a key element before capturing |
| Screenshot cuts off content | Only the viewport was captured | Use a full-page workaround or a full_page=true API call |
| Element not found for element screenshot | Selector changed or element loads late | Re-check the selector, wait for element visibility |
| Blurry image on retina displays | Device pixel ratio not set | Set pixel ratio in browser options, or use a retina parameter |
| Screenshot shows a cookie banner | No blocking configured | Add cookie-banner blocking logic, or use no_cookie_banners=true |
| Inconsistent results across machines | Different browser/driver versions | Pin driver versions, or move to a managed rendering service |
Selenium's screenshot methods work well for what they were built for: quick, in-test visual evidence tied to a specific viewport. The moment you need full-page captures, cross-browser consistency, scheduled monitoring, or screenshots at volume, you're writing and maintaining infrastructure that a managed API already handles. If that sounds like where your project is heading, try ScreenshotAPI.net for free and see how a single request compares to your current setup.
Firefox's driver supports it through get_full_page_screenshot_as_file(). Chrome and Edge don't have a built-in equivalent, so full-page capture on Chromium browsers needs a CDP-based workaround or a stitching approach.
Yes. Both Python and Java expose element-level screenshot methods (element.screenshot() in Python, or capturing a WebElement's bounding box in Java), which crop the image to just that element.
Usually because the capture fires before the page has finished loading or before JavaScript has hydrated the content. Replace fixed sleep() calls with explicit waits tied to a specific element or condition.
Not for functional testing. Selenium still owns interaction testing (clicks, form fills, assertions). A screenshot API is a better fit specifically for the capture step, particularly at scale, in monitoring pipelines, or for AI agents that just need a rendered view of a page.