How to take a website screenshot with Java (Selenium, Playwright, API)
I kept putting off the Java tutorial because I expected mountains of boilerplate. Turns out Java's built-in HttpClient makes API calls almost as clean as Python's requests. Here are three ways to capture website screenshots with Java — Selenium, Playwright, and an API — with working code, honest comparison, and a Spring Boot integration example.
I've already written screenshot guides for Python, Node.js, PHP, Go, and cURL, but Java kept getting pushed to the bottom of the list — not because it's hard, but because I kept picturing mountains of boilerplate code and figured it would be tedious to put together. Turns out the built-in HttpClient from Java 11+ makes HTTP calls almost as clean as Python's requests, and I was genuinely surprised how little code the final version needed. If you already have a Spring Boot project running, adding screenshot capture is maybe a ten-minute job.
In this guide I'll walk through three ways to take website screenshots with Java: Selenium WebDriver, Playwright for Java, and a screenshot API. For each one I'll show you working code you can run right now, explain where the approach works best, and be honest about the limitations you'll run into.
What you'll need before getting started
Every example in this guide requires Java 17 or higher and Maven — check that they're installed:
java --version
mvn --versionJava 17 is the current LTS that most teams have already migrated to, so chances are you already have it. If you're still on Java 11, everything in this guide will work there too, except a couple of syntax shortcuts like List.of(). Let's start by creating a project:
mvn archetype:generate \
-DgroupId=com.example.screenshots \
-DartifactId=java-screenshots \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
cd java-screenshotsThis gives you a standard Maven project with a src/main/java folder and a pom.xml where we'll add dependencies as we go. Now let's move on to the actual screenshots.
Method 1: Selenium WebDriver — when it's already in your project
Selenium is the tool most Java developers already know from their UI testing days, and if it's already in your project, adding screenshots to existing code is literally a single method call. It's a different story if you're installing Selenium from scratch just for screenshots — in that case, it's probably overkill, because the amount of boilerplate you end up writing feels disproportionate for such a simple task.
Which dependencies to add to your pom.xml
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.27.0</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.2</version>
</dependency>
</dependencies>
The webdrivermanager library automatically downloads the right ChromeDriver binary and keeps it in sync with your installed browser version. Without it, you'd have to manually track chromedriver releases and match versions every time Chrome updates — a chore that nobody enjoys and that regularly breaks CI pipelines.
How to take a basic screenshot with Selenium
Create a file called SeleniumScreenshot.java:
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 io.github.bonigarcia.wdm.WebDriverManager;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class SeleniumScreenshot {
public static void main(String[] args) throws IOException {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--window-size=1280,800");
WebDriver driver = new ChromeDriver(options);
driver.get("https://dev.to");
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Files.copy(screenshot.toPath(), Path.of("dev_to.png"),
StandardCopyOption.REPLACE_EXISTING);
System.out.println("Saved: dev_to.png");
driver.quit();
}
}Run it with:
mvn compile exec:java -Dexec.mainClass="com.example.screenshots.SeleniumScreenshot"The browser starts in headless mode, loads dev.to, captures whatever fits in the 1280×800 viewport, and saves the result as a PNG file. That cast to TakesScreenshot looks, honestly, not very elegant — it's been there since Selenium 2, and after all these years nobody has redesigned the API into something cleaner. But it works reliably.
How to capture only a specific element on the page
Sometimes you don't need the entire page, just one particular block — a site header, a product card, a chart, or a form. Selenium can capture individual elements, and it's almost as straightforward:
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
// ... same setup as above ...
driver.get("https://dev.to");
WebElement header = driver.findElement(By.tagName("header"));
File headerShot = header.getScreenshotAs(OutputType.FILE);
Files.copy(headerShot.toPath(), Path.of("dev_to_header.png"),
StandardCopyOption.REPLACE_EXISTING);
System.out.println("Saved: dev_to_header.png");
driver.quit();The result is a neatly cropped image showing just the header — navigation, logo, search field, and nothing else. This works well when you need to capture a login form for documentation, a pricing table for a report, or any other specific block without the rest of the page getting in the way. If you want to go deeper into this, I wrote a separate guide on how to capture a specific element using a CSS selector.
Why Selenium isn't the right tool for full-page screenshots
Selenium has one notable limitation — it can't take full-page screenshots. The getScreenshotAs() method only captures the visible viewport, and everything below the fold simply doesn't make it into the image. There are hacky workarounds involving JavaScript scrolling and image stitching, but in practice they're fragile and not worth the time. If you need full-page captures, there's a much better tool for the job — Playwright.
Method 2: Playwright for Java — full-page screenshots and mobile emulation out of the box
Playwright is Microsoft's browser automation library, and its Java version is surprisingly well-made. For the screenshot task specifically, Playwright has one fundamental advantage over Selenium — full-page captures work out of the box with a single parameter, no hacks or stitching required.
How to add Playwright to a Maven project
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.49.0</version>
</dependency>After adding the dependency, you still need to download the browser itself, which is done with a single command:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium"This downloads a Chromium binary weighing about 400 MB — it only happens once, but it's worth keeping in mind if you're building a Docker image, because that's 400 MB on top of your container.
A basic Playwright screenshot — noticeably less code
Create PlaywrightScreenshot.java:
import com.microsoft.playwright.*;
import java.nio.file.Paths;
public class PlaywrightScreenshot {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.setViewportSize(1280, 800);
page.navigate("https://news.ycombinator.com");
page.screenshot(new Page.ScreenshotOptions()
.setPath(Paths.get("hackernews.png")));
System.out.println("Saved: hackernews.png");
}
}
}
The code is noticeably shorter than Selenium — no driver management, no clunky casting, no manual file copying. Playwright cleans up after itself through try-with-resources, so when the block ends, the browser shuts down automatically. But the real advantage of Playwright is still ahead.
Full-page screenshot — the main reason to choose Playwright over Selenium
This is really what makes Playwright worth using instead of Selenium. One parameter — setFullPage(true) — and you get a screenshot of the entire page from the very top to the very bottom:
page.navigate("https://news.ycombinator.com");
page.screenshot(new Page.ScreenshotOptions()
.setPath(Paths.get("hackernews_full.png"))
.setFullPage(true));
System.out.println("Full-page screenshot saved");
Playwright scrolls through the page on its own, waits for content, and assembles one tall image. For Hacker News the result comes out at around 3000 pixels tall — all 30 stories plus the footer with the search bar. Try doing the same thing in Selenium with one line of code. You can't.
How to emulate a mobile device for your screenshot
Playwright has a built-in device registry, so emulating a phone is something you can do without manually looking up viewport sizes and user agent strings:
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
BrowserContext context = browser.newContext(
new Browser.NewContextOptions()
.setViewportSize(390, 844)
.setDeviceScaleFactor(3)
.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)")
.setIsMobile(true)
);
Page page = context.newPage();
page.navigate("https://news.ycombinator.com");
page.screenshot(new Page.ScreenshotOptions()
.setPath(Paths.get("hackernews_mobile.png")));
System.out.println("Mobile screenshot saved");
}
Width 390 and height 844 match the iPhone 14 screen, and the setIsMobile(true) flag switches the browser into mobile mode where responsive styles kick in and touch events get enabled. The page looks exactly like it would on a real phone, and you don't need to hunt down viewport dimensions yourself — Playwright handles the device profile for you. For more viewport sizes of popular phones and tablets, check out my guide on mobile device screenshots.
How to wait for dynamic content to load before taking the screenshot
JavaScript-heavy pages have a common problem — the screenshot fires before the content has finished loading, and you end up with a half-empty page or a spinner instead of actual data. Playwright can wait for a specific element to appear:
page.navigate("https://github.com/trending");
page.waitForSelector("article.Box-row");
page.screenshot(new Page.ScreenshotOptions()
.setPath(Paths.get("github_trending.png")));
With waitForSelector, Playwright pauses execution until the target element shows up in the DOM. Without this line you'd most likely get the page skeleton with an empty repository list. This kind of waiting is especially important for SPA applications and any pages that fetch data after the initial HTML load.
Method 3: Screenshot API — when you don't want to deal with browsers on your server
Both Selenium and Playwright require a real browser running on your machine, and on a local dev box that's perfectly fine. But on a production server, in a Lambda function, or inside a CI pipeline the story changes completely — Chrome pulls in system libraries for rendering, eats 200-400 MB of RAM per instance, and sometimes crashes for no obvious reason, simply because it ran out of memory.
A screenshot API takes a different approach: you send an HTTP request with a URL and get a finished image back. No browsers to install, no RAM to allocate, no chromedriver versions to keep in sync.
What this looks like with Java's built-in HttpClient
I'll show this using ScreenshotRun — the API I built for exactly this kind of task. The free plan gives you 200 requests per month, which is plenty to try the approach and decide whether it works for your project. Sign up and grab your API key from the dashboard.
The nice thing about Java 11+ is that HttpClient is built into the standard library, so you don't need to pull in any external dependencies at all.
Create ApiScreenshot.java:
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class ApiScreenshot {
public static void main(String[] args) throws IOException, InterruptedException {
String apiKey = "your-api-key-here";
String targetUrl = "https://producthunt.com";
String query = String.format(
"url=%s&format=png&width=1280&height=800&response_type=image",
URLEncoder.encode(targetUrl, StandardCharsets.UTF_8)
);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://screenshotrun.com/api/v1/screenshots/capture?" + query))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(Path.of("producthunt.png"))
);
if (response.statusCode() == 200) {
System.out.println("Screenshot saved: producthunt.png");
} else {
System.out.println("Error: " + response.statusCode());
}
}
}
Run it:
mvn compile exec:java -Dexec.mainClass="com.example.screenshots.ApiScreenshot"
Zero external dependencies — the entire thing runs on Java's standard library. The HttpResponse.BodyHandlers.ofFile() method streams the response body directly to disk, so you're not loading the entire image into memory at once, and that matters when you're capturing full-page screenshots of heavy sites where those PNG files can be several megabytes.
Notice the response_type=image parameter — it tells the API to return raw image bytes without a JSON wrapper. If you need metadata (dimensions, file size, render time), you can use response_type=json instead and parse the JSON response.
How to get a full-page screenshot from the API with one extra parameter
For a full-page capture, just add full_page=true:
String query = String.format(
"url=%s&format=png&width=1280&full_page=true&response_type=image",
URLEncoder.encode("https://github.com/topics", StandardCharsets.UTF_8)
);
// ... same HttpClient setup ...
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(Path.of("github_topics_full.png"))
);
The API scrolls through the page on its own, waits for lazy-loaded images, and renders all JavaScript. You don't need to write any scroll logic or set up timeouts — that's the whole point of outsourcing screenshots to an API. If you've read my post on fixing blank images in full-page screenshots, you know how many edge cases come up in practice, and the API handles them for you.
Mobile screenshot — just specify the viewport dimensions
String query = String.format(
"url=%s&format=png&width=390&height=844&response_type=image",
URLEncoder.encode("https://stripe.com", StandardCharsets.UTF_8)
);
Same idea as Playwright's mobile emulation, but without having to start a browser. Width 390 and height 844 give you an iPhone 14 viewport, and you can find dimensions for other popular phones and tablets in my guide on capturing sites as they appear on mobile devices.
Batch processing multiple URLs in a single run
This is where the API really wins compared to browser-based approaches. With Selenium or Playwright, each URL means launching or reusing a browser instance, managing memory, and handling timeouts. With an API, each URL is just another ordinary HTTP request, no different from calling any other REST service.
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class BatchScreenshots {
public static void main(String[] args) throws IOException, InterruptedException {
String apiKey = "your-api-key-here";
List<String> urls = List.of(
"https://github.com",
"https://stackoverflow.com",
"https://dev.to",
"https://news.ycombinator.com",
"https://producthunt.com"
);
Files.createDirectories(Path.of("screenshots"));
HttpClient client = HttpClient.newHttpClient();
for (String url : urls) {
String domain = url.split("//")[1]
.split("/")[0]
.replace(".", "_");
String query = String.format(
"url=%s&format=png&width=1280&height=800&response_type=image",
URLEncoder.encode(url, StandardCharsets.UTF_8)
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://screenshotrun.com/api/v1/screenshots/capture?"
+ query))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(
Path.of("screenshots/" + domain + ".png"))
);
if (response.statusCode() == 200) {
System.out.println("✓ " + domain);
} else {
System.out.println("✗ " + domain + ": " + response.statusCode());
}
Thread.sleep(1000); // respect rate limits
}
System.out.println("\nDone — check screenshots/");
}
}Five URLs, five files, and the code still reads without effort. The Thread.sleep(1000) pause between requests keeps you from hitting the rate limit — without it, some requests may come back with a 429 error, especially on the free plan. If speed matters more, the paid plans have higher limits on requests per second.
For more on handling rate limits and caching repeated requests, I wrote a separate guide on how to cache screenshots and stop paying for the same capture twice.
How to integrate screenshot capture into a Spring Boot application
Most Java developers aren't writing standalone scripts — they're building web applications, and in that context screenshots are usually needed as a service that other parts of the system can call. Here's how to add a screenshot endpoint to a Spring Boot project so your app becomes a proxy server that your frontend or other microservices can hit.
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/api/screenshots")
public class ScreenshotController {
private static final String API_KEY = "your-api-key-here";
private final HttpClient httpClient = HttpClient.newHttpClient();
@GetMapping("/capture")
public ResponseEntity<byte[]> capture(
@RequestParam String url,
@RequestParam(defaultValue = "1280") int width,
@RequestParam(defaultValue = "800") int height,
@RequestParam(defaultValue = "png") String format
) throws IOException, InterruptedException {
String query = String.format(
"url=%s&format=%s&width=%d&height=%d&response_type=image",
URLEncoder.encode(url, StandardCharsets.UTF_8),
format, width, height
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://screenshotrun.com/api/v1/screenshots/capture?"
+ query))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<byte[]> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofByteArray()
);
if (response.statusCode() != 200) {
return ResponseEntity.status(response.statusCode())
.body(null);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(
format.equals("png") ? MediaType.IMAGE_PNG : MediaType.IMAGE_JPEG);
return new ResponseEntity<>(response.body(), headers, HttpStatus.OK);
}
}Now your app has a /api/screenshots/capture?url=https://example.com endpoint that returns a screenshot image directly. Your frontend can plug this URL straight into an <img> tag, and on top of that you could add caching through Redis or local disk, authentication, and your own rate limiting — though even this bare-bones version is already functional.
The pattern is similar to what I described in my post about adding website preview thumbnails to a link directory — the API handles the heavy rendering, and your app just proxies the finished result.
Selenium, Playwright, and API — an honest comparison of all three approaches
I've tried all three in Java and want to share my impressions, because each one is good in its own situation, and there's no universal answer here.
When it makes sense to use Selenium
Selenium works best if it's already in your project for UI tests — adding getScreenshotAs() to existing test code takes exactly one line. But if you're installing Selenium from scratch just for screenshots, you end up with too much boilerplate for such a simple result.
On the plus side, it's the most documented browser automation tool in the Java ecosystem, with tons of StackOverflow answers and tutorials for every situation. It supports Chrome, Firefox, Safari, and Edge, and it lets you interact with the page before capturing — clicking buttons, filling forms, scrolling content. It integrates well with JUnit and TestNG, which makes sense since Selenium was originally built for testing.
On the other hand, there are no full-page screenshots, you need WebDriverManager or manual chromedriver management, and the API design is showing its age — the TakesScreenshot cast alone is pretty rough. Each browser instance also consumes 200-400 MB of RAM.
When Playwright is the better choice
Playwright is the strongest option if you're starting from scratch and need full-page screenshots, mobile emulation, or reliable waiting for dynamic content. For local scripts and CI pipelines with moderate volume, it felt like the most capable of the three to me.
Full-page captures activate with a single setFullPage(true) parameter, there are built-in mobile device profiles, and waitForSelector lets you wait for specific content before taking the shot. Playwright's API is noticeably cleaner than Selenium's — less boilerplate and automatic resource cleanup through try-with-resources. And it works the same way across Chromium, Firefox, and WebKit.
The tradeoffs are that the Chromium binary is around 400 MB, each browser instance eats 200-400 MB of RAM, and on servers you still need system rendering libraries. The Java SDK also gets updates a bit later than the Node.js version, though in practice this rarely causes issues.
When a Screenshot API makes the most sense
An API is the right fit for server-side automation, batch jobs, and Spring Boot applications — essentially any situation where you don't want to install or maintain browsers on your infrastructure and you need predictable performance.
The main advantage is zero dependencies beyond the built-in HttpClient that comes with every Java 11+ JDK. It works anywhere — VPS, Lambda, Kubernetes, your laptop — and creates no RAM overhead on your side. The API handles lazy loading, hides cookie banners, and renders JavaScript on its own, and scaling from one screenshot to ten thousand requires no architectural changes.
The limitations are that you need a network connection, the free plan on ScreenshotRun has a 200 requests/month cap, and each request takes 3-10 seconds due to network and render time. You also can't interact with the page — no clicking, no logging in, just capture by URL. Paid plans start at $9/month for higher volumes.
Which approach to pick in the end
For screenshots inside your existing test suite, Selenium makes sense — it's already there, and adding one line is faster than pulling in a new dependency. For local scripts and CI where you need full-page captures, I'd go with Playwright — it's simply more powerful and cleaner. And for production services, Spring Boot apps, and batch processing, a Screenshot API is the way to go, because you don't want to drag a browser into your production infrastructure.
A few practical tips that will save you debugging time
While putting this guide together, I learned a few things that are worth knowing upfront.
Your Java version determines which HTTP client you'll use. The java.net.http.HttpClient class was introduced in Java 11 as part of JEP 321, and if you're stuck on Java 8 for some reason, you'll need to use Apache HttpClient or OkHttp instead — both work fine, but you'll end up writing noticeably more code.
Set timeouts right away rather than waiting for a request to hang. Rendering screenshots of heavy pages can take anywhere from 3 to 10 seconds, and a reasonable timeout on your HTTP client will save you from situations where a request hangs indefinitely:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();Parallel browser instances eat through memory fast. If you're running ten Selenium or Playwright instances in parallel, expect 2-4 GB of RAM consumed by screenshots alone. With the API approach, your memory usage stays near zero because rendering happens on a remote server.
Pick your file format based on the task at hand. PNG gives you pixel-perfect images with no compression artifacts, making it the best choice for screenshots where text readability matters. JPEG files are smaller but lossy — fine for thumbnails and previews where small details aren't as important. I wrote more about this in my post on screenshot caching strategies.
Cookie banners ruin almost every screenshot you take. In Selenium and Playwright you can inject JavaScript to hide banners before capturing, but every site implements its banner differently and maintaining those scripts gets tedious. ScreenshotRun's API blocks common cookie consent dialogs automatically, without any extra code on your side.
If you only need a specific part of the page, you can target it with a CSS selector. Take a look at my guide on capturing a specific element by CSS selector — both Playwright and the ScreenshotRun API support element-level captures rather than screenshotting the whole page.
That's everything — three approaches, working code for each, and honest tradeoffs. Pick the one that fits your stack and your use case. If you want to try the API route without setting anything up, ScreenshotRun has a free tier — 200 screenshots a month, no credit card needed.
Thanks for reading, and I hope this guide saves you some time on your next Java project.
Vitalii Holben