ScreenshotNeo

BlogHow-to

How to Use Selenium to Capture Website Screenshots for Visual Change Detection

Capture repeatable Selenium screenshots, compare them with approved baselines, and troubleshoot visual noise in your regression checks.

By the ScreenshotNeo team4 October 20269 min read

Selenium can capture a browser page or a specific element as a PNG. To detect visual changes, save a screenshot from a controlled browser state, then compare it with a previously reviewed baseline. Screenshot capture alone does not perform the comparison.

This guide builds a small Python workflow: install Selenium and Pillow, capture a page after a condition-based wait, compare it to a baseline, and save a diff image for review. It also covers the rendering conditions, dynamic content, and common failures that can make comparisons noisy.

1. Set up a repeatable capture environment

A visual check is meaningful only when its inputs are consistent. Keep the browser version, operating system, headless setting, viewport, page state, and capture scope aligned between baseline creation and later runs. Rendering can vary across environments; Playwright’s visual comparison guidance lists several environmental sources of variation, including browser version, operating system, settings, and headless mode. Playwright: Visual comparisons

Install the dependencies in a virtual environment:

python -m venv .venv
# macOS or Linux:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip install selenium pillow

Selenium Manager can obtain a compatible driver for supported browser setups when Selenium starts the browser. Use a pinned browser and driver arrangement in CI if you need stable rendering across runs; the exact installation method depends on your runner and browser policy.

Choose the capture scope before making a baseline:

  • Whole viewport: catches changes across the visible browsing context but includes unrelated dynamic areas.
  • One element: focuses on a component and can reduce noise from unrelated page regions. It will not catch surrounding layout changes outside that element.

Selenium documents browser window management and screenshot examples, including the effect of screen resolution and element screenshots. Selenium: Working with windows and tabs

2. Capture the page after it reaches the state you want to check

Navigation completing does not guarantee that a JavaScript application has finished rendering. Wait for a page-specific condition that represents visual readiness, such as a heading appearing or a loading indicator disappearing. Selenium’s wait guide explains explicit waits and warns that mixing implicit and explicit waits can produce unpredictable timing. Selenium: Waiting Strategies

Save this as capture.py. Set BASELINE=1 to create the initial reference image; subsequent runs capture the current page for comparison. Update the URL, readiness selector, and viewport for your application.

import os
from pathlib import Path

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

URL = os.environ.get("TARGET_URL", "https://example.com")
BASELINE_MODE = os.environ.get("BASELINE", "0") == "1"
Path("artifacts").mkdir(exist_ok=True)

options = webdriver.ChromeOptions()
# Keep this setting the same when generating and checking the baseline.
options.add_argument("--headless")
options.add_argument("--window-size=1440,1000")

driver = webdriver.Chrome(options=options)
try:
    driver.set_window_size(1440, 1000)
    driver.get(URL)

    # Replace this with a selector that means the page is visually ready.
    WebDriverWait(driver, 20).until(
        EC.visibility_of_element_located((By.TAG_NAME, "h1"))
    )

    # Optional: wait for a known loading marker to disappear.
    # WebDriverWait(driver, 10).until(
    #     EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading"))
    # )

    output = "artifacts/baseline.png" if BASELINE_MODE else "artifacts/current.png"
    if not driver.save_screenshot(output):
        raise RuntimeError(f"Screenshot save failed: {output}")
    print(f"Saved {output}")
finally:
    driver.quit()

Run the baseline capture once, review the resulting file, then retain it as the approved reference:

BASELINE=1 TARGET_URL=https://example.com python capture.py
# Windows PowerShell:
# $env:BASELINE="1"; $env:TARGET_URL="https://example.com"; python capture.py

To capture only a component, replace the screenshot call with an element screenshot after locating and waiting for the element:

panel = WebDriverWait(driver, 20).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "main .pricing-panel"))
)
panel.screenshot("artifacts/pricing-panel.png")

Element screenshots are useful when the test concerns that component. Choose whole-page context when the test must also detect positioning, surrounding layout, or interactions with nearby content. The cited Selenium API documents page and element capture methods; the project must decide which region is important to its regression risk. Selenium WebDriver API

3. Compare the new screenshot with a reviewed baseline

Here is a basic, runnable pixel comparison using Pillow. It requires equal image dimensions, writes a visual diff, and reports the share of pixels that differ. The threshold is a project setting, not a Selenium guarantee. Start by inspecting diffs and deciding what level of variation is acceptable for your page and environment.

Save this as compare.py:

from pathlib import Path
from PIL import Image, ImageChops

baseline_path = Path("artifacts/baseline.png")
current_path = Path("artifacts/current.png")
diff_path = Path("artifacts/diff.png")

baseline = Image.open(baseline_path).convert("RGB")
current = Image.open(current_path).convert("RGB")
if baseline.size != current.size:
    raise SystemExit(
        f"Image sizes differ: baseline={baseline.size}, current={current.size}. "
        "Check viewport, browser chrome, and capture scope."
    )

diff = ImageChops.difference(baseline, current)
diff.save(diff_path)
# A pixel differs if any RGB channel differs. This is strict exact-pixel comparison.
different_pixels = sum(1 for pixel in diff.getdata() if pixel != (0, 0, 0))
total_pixels = baseline.width * baseline.height
ratio = different_pixels / total_pixels
print(f"Different pixels: {different_pixels}/{total_pixels} ({ratio:.4%})")
print(f"Diff image: {diff_path}")

# Example policy only. Tune for the page and review false positives.
allowed_ratio = 0.001
if ratio > allowed_ratio:
    raise SystemExit("Visual comparison exceeded the configured tolerance")

Run capture and comparison:

TARGET_URL=https://example.com python capture.py
python compare.py

This simple metric treats every changed RGB pixel equally. It can flag harmless font rasterization or a blinking cursor and does not understand whether a change is visually important. For a real check, first stabilize the page and environment, then validate any tolerance or masking rule against both known-good and intentionally changed examples. Chromium’s pixel-testing guide describes the role of approved images and baseline updates in its own workflow. Chromium: Pixel tests

4. Keep captures stable between runs

  1. Fix the viewport and capture scope. Use the same window dimensions and the same page or element screenshot method every time.
  2. Wait for meaningful readiness. Prefer an explicit wait for the content under test. A fixed sleep can be too short on a slow run and waste time on a fast one.
  3. Control changing content. Use test data or a stable route where possible. Consider hiding or masking timestamps, rotating promotions, live counters, cursors, and other regions that are intentionally variable. Make the policy explicit and ensure masked areas are outside the visual risk being tested.
  4. Control animation. Wait for transitions to finish, or use a test-only setting to disable motion if your application supports one. Capture at a defined point in an animation if the animation itself is under test.
  5. Record context. Store the route, viewport, browser version, test state, and timestamp alongside baseline and diff artifacts so a reviewer can reproduce the capture. This is workflow advice based on documented environmental variation, not a required Selenium file format.
  6. Review before updating. A mismatch is evidence to inspect, not proof of a defect. Accept a new baseline only after confirming the visual change is intentional.

5. Common errors and fixes

Symptom Likely cause What to do
TimeoutException waiting for an element The selector is wrong, the element is hidden, or the page has not reached the assumed state. Check the selector and page state in the browser. Wait for the element that actually signals readiness; increase the timeout only if the state legitimately takes longer.
Screenshot is blank or incomplete The app is still rendering, content is below the viewport, or a new tab/frame is active. Wait for the relevant content; switch to the expected window or frame. For a viewport capture, ensure the target is inside the viewport. Selenium’s standard screenshot captures the current browsing context.
Baseline and current images have different sizes Viewport, browser window sizing, device scale, or capture scope changed. Use the same browser configuration and dimensions, then regenerate the baseline only if the new dimensions are intended.
Diff fails every run despite no apparent design change Dynamic data, animations, font rendering, browser or OS variation, or asynchronous updates produce pixel changes. Stabilize those inputs, align rendering environments, wait for the final state, and inspect the diff before defining a tolerance or mask.
Chrome or driver fails to start Browser installation, driver resolution, container permissions, or headless configuration is incompatible. Confirm Chrome is installed and accessible, use a compatible Selenium/browser setup, and inspect the complete driver error. Keep CI browser configuration consistent.
Screenshot file is missing Output directory does not exist, path differs from the working directory, or save returned false. Create the directory before capture, use an explicit path, check the return value, and preserve the test’s working directory.
Wait behaves unpredictably Implicit and explicit waits are mixed, or the condition does not represent visual readiness. Use one deliberate waiting strategy, generally explicit condition-based waits for this workflow. Selenium cautions against mixing implicit and explicit waits.

6. Performance, reliability, and cost

Each WebDriver session starts a browser, navigates, waits, and writes image data, so the page load and readiness condition usually dominate the comparison step. Keep captures focused on the visual risk, reuse a session for a related sequence where appropriate, and always close it in a finally block. Avoid parallelizing captures against the same mutable test account or data, since state collisions make results unreliable.

For reliable CI results, preserve screenshots and diffs as build artifacts, make failures reproducible, and treat baseline changes as reviewed code changes. A screenshot mismatch should fail or flag the check according to your team’s policy; it should not silently replace the approved image. Selenium and the image comparison code shown here do not impose a screenshot service charge. Costs instead depend on the infrastructure running the browser, CI minutes, and storage you choose.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return an image or PDF; its screenshot API can be used when you want a captured page without managing a Selenium browser session. It is not a visual baseline comparison engine, so keep your existing comparison step if you need change detection. See the ScreenshotNeo API documentation.

cURL:

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,
)
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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

Cookie and consent banners are accepted before capture, and 60+ known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, with response headers indicating page verdict and billing. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

8. FAQ

Does Selenium detect visual changes by itself?

No. WebDriver captures the image. Your test must compare it to an approved baseline and decide how to handle mismatches.

Should I use a full-page or element screenshot?

Use a whole-context capture when surrounding layout matters. Use an element capture when the target component is the test subject and unrelated regions would add noise.

Can I compare screenshots from different operating systems?

You can, but rendering differences can create noise. For a stable baseline, keep operating system and browser conditions aligned, or validate a comparison policy designed for cross-environment differences.

Is a pixel mismatch always a regression?

No. It can reflect an intended design update or rendering variation. Inspect the diff and review a baseline update before accepting it.