Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Selenium to put a page in a known state, capture the rendered browser view, compare it with an approved baseline, and review the difference before accepting or rejecting the change. A reliable CSS visual-regression test is a controlled workflow—not just a call to save a PNG. The examples below show a deterministic Python setup, baseline and diff handling, dynamic-content controls, CI practices, and recovery when screenshots are flaky.

The visual-regression loop

Every screenshot test should answer one question: did this UI state change unexpectedly? The repeatable loop is:

  1. Start a known browser, viewport, locale, and test data.
  2. Navigate to the route or state under test.
  3. Wait for an application-specific ready condition.
  4. Capture the browser window or a target element.
  5. Compare the image with an approved baseline.
  6. Inspect the diff and either fix the CSS/behavior or approve a new baseline.

This is the same checkpoint-and-review model described in Applitools’ visual testing overview: an image difference is evidence for a decision, not an automatic reason to overwrite the reference image.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What Selenium captures in Python

Selenium’s WebDriver screenshot API captures the current browsing context (normally the visible browser window). It is not, by itself, a guaranteed full-page screenshot method. You can also capture one element with element.screenshot(). The official examples are documented in Selenium’s browser screenshot documentation and the Python WebDriver API reference.

Install the test dependencies

python -m pip install selenium pillow

Selenium Manager can obtain a compatible driver for current Selenium releases. In a locked-down CI image, install and pin the browser and driver through that image instead. The comparison code below uses Pillow to inspect pixels; choose and pin a maintained image-diff library if your project needs perceptual thresholds, masks, or richer reports.

Capture a deterministic checkpoint

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

OUTPUT = Path("artifacts/homepage.png")
OUTPUT.parent.mkdir(parents=True, exist_ok=True)

driver = webdriver.Chrome()
try:
    driver.set_window_size(1280, 900)
    driver.get("https://example.com")
    WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "main"))
    )
    if not driver.save_screenshot(str(OUTPUT)):
        raise OSError(f"Could not write {OUTPUT}")
finally:
    driver.quit()

save_screenshot writes PNG data and returns False on an I/O error. The explicit wait matters because navigation completion does not mean that client-side rendering, data fetching, fonts, or transitions have finished. Selenium documents this race in its waiting strategies guide: sometimes the browser reaches the required state first, and sometimes Selenium executes first.

Capture only the component under test

from selenium.webdriver.common.by import By

card = driver.find_element(By.CSS_SELECTOR, "[data-testid='pricing-card']")
card.screenshot("artifacts/pricing-card.png")

Element captures reduce unrelated page noise and make a CSS component test easier to review. Keep a page-level checkpoint as well when layout interactions matter.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make the browser state reproducible

Pixel comparison is sensitive to inputs that ordinary functional tests can ignore. Baseline and candidate runs should use the same:

  • browser family and version;
  • operating-system image and installed fonts;
  • viewport dimensions and device scale factor;
  • color scheme, timezone, locale, and reduced-motion preference;
  • authentication state and seeded data;
  • network fixtures or API responses where practical.

Set these values in one fixture rather than scattering them through tests. A fixed viewport is especially important: a responsive breakpoint can legitimately produce a completely different layout.

Freeze or remove unstable content

Animations, rotating adverts, timestamps, random IDs, live counters, and personalized data create differences that are not CSS regressions. Prefer deterministic fixtures. If you control the application, add a test mode that disables animation and substitutes fixed data. For third-party widgets, hide or stub them at the network or application boundary.

Hosted visual tools may provide screenshot-only CSS and ignored regions. For example, the documented Percy Python Selenium integration describes custom CSS, responsive widths, full-page options, frozen animated images, and ignored regions. Those options are specific to that integration; verify current behavior and plan terms before adopting them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Baseline storage and image comparison

Give every checkpoint a stable name such as home--logged-out--desktop. Store approved references in version control for a small suite, or in CI artifact storage when the images are numerous. Keep candidate, diff, and diagnostic metadata when a test fails.

A transparent local comparison

The following example compares equal-sized RGB PNGs and writes a red-highlighted diff. It fails when more than 0.1% of pixels differ. The threshold is an example policy; calibrate it against your rendering environment rather than treating it as a universal standard.

from pathlib import Path
from PIL import Image, ImageChops


def compare_images(baseline_path, candidate_path, diff_path, max_fraction=0.001):
    baseline = Image.open(baseline_path).convert("RGBA")
    candidate = Image.open(candidate_path).convert("RGBA")
    if baseline.size != candidate.size:
        raise AssertionError(
            f"size changed: baseline={baseline.size}, candidate={candidate.size}"
        )

    diff = ImageChops.difference(baseline, candidate)
    changed = 0
    for pixel in diff.getdata():
        if any(channel != 0 for channel in pixel[:3]):
            changed += 1
    total = baseline.width * baseline.height
    fraction = changed / total

    diff.save(diff_path)
    if fraction > max_fraction:
        raise AssertionError(
            f"visual difference {fraction:.3%} exceeds {max_fraction:.3%}; "
            f"see {diff_path}"
        )

compare_images(
    "baselines/homepage.png",
    "artifacts/homepage.png",
    "artifacts/homepage-diff.png",
)

This exact-pixel approach is intentionally simple and auditable. Anti-aliasing or font rasterization can produce one-pixel changes, so teams often add masks, a small color tolerance, or a perceptual comparison after proving that the environment is stable. Do not silently raise the threshold until real defects disappear; record why the rule changed.

The review decision

  • Unexpected difference: keep the baseline, attach the candidate and diff to the failed CI job, and fix the implementation.
  • Intentional design change: review the diff, then replace the baseline in the same change that updates the CSS or markup.
  • Unexplained intermittent difference: rerun with identical inputs and investigate readiness, animation, data, fonts, and browser versions before changing any image.

Waiting for the right state

Use an explicit condition that represents readiness for the checkpoint, such as a visible component, a loading indicator disappearing, or a test-only “ready” marker. Selenium’s expected conditions include visibility and presence checks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
wait = WebDriverWait(driver, 15)
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "main")))
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))

A fixed sleep can be useful for a short, known transition, but it is less reliable than waiting for the condition that actually matters. If fonts or images alter layout after the main element appears, add an application-specific readiness signal or wait for those resources in your test environment.

Viewport, element, and full-page scope

Viewport screenshots

driver.save_screenshot() captures the current window. It is the most predictable scope for responsive CSS checks because the viewport dimensions are explicit.

Element screenshots

element.screenshot() is useful for buttons, cards, navigation bars, and isolated components. Ensure the element is visible and not covered by a sticky overlay before capture.

Full-page images

Do not describe the basic Python WebDriver call as a standard full-page capture. Browser-specific implementations may offer full-page behavior, while scrolling and stitching can introduce seams or duplicate floating bars. Applitools' screenshotting guidance, published December 18, 2018, discusses these anomalies and its own controls; treat that as vendor-specific guidance, not a Selenium guarantee. If you need a full document image, validate the method in your exact browser and inspect sticky elements, lazy-loaded content, and scroll-triggered effects.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

CI execution and failure artifacts

Run the same test command locally and in CI. Use a fixed container or virtual machine image, pin browser versions, and publish candidate, baseline, and diff files as artifacts. A failure message should include the checkpoint name, viewport, browser version, image dimensions, difference rule, and paths to the artifacts.

Parallel jobs are safe when each job writes to an isolated artifact directory. Do not let concurrent runs update baselines automatically. Protect baseline changes with code review, and keep the CSS/markup change and approved image in one commit so reviewers can understand the visual intent.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting intermittent screenshot failures

The screenshot is blank or incomplete

Cause: the capture ran before the application rendered, an iframe was not ready, or a navigation failed. Fix: assert the URL and a meaningful visible element, wait for the loading state to end, and save browser logs or page HTML on failure.

Only text or fonts differ

Cause: different installed fonts, late web-font loading, operating-system rasterization, or a device-scale mismatch. Fix: use the same image and font environment, wait for font readiness in the application, and keep the device scale factor consistent.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Differences move between runs

Cause: animation, timestamps, random data, ads, chat widgets, or live API responses. Fix: freeze time and data, disable motion in test mode, stub third-party requests, or mask a region whose content is intentionally outside the test's scope.

The candidate dimensions do not match

Cause: a changed viewport, browser chrome configuration, responsive breakpoint, or full-page stitching method. Fix: set the window size before navigation, log the actual screenshot dimensions, and compare like-for-like scopes.

A failure appears after an intentional redesign

Review the diff rather than accepting it blindly. If the new appearance is correct, update the named baseline alongside the redesign. If only part of the image changed unexpectedly, preserve the baseline and fix that regression.

Hosted review options

A local workflow keeps storage and comparison mechanics under your control. A hosted service can add checkpoint dashboards, baseline approval, artifact retention, and browser or responsive matrices. Percy documents a direct Selenium Python call, percy_snapshot(driver, name), plus the capture controls noted earlier. Applitools documents checkpoints, baseline management, and review in its visual testing overview. Current pricing, limits, data handling, and feature parity change, so confirm those details in each provider's current documentation before selecting one.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server when you need a rendered URL without maintaining a Selenium browser. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. AI clients such as Claude, Cursor, and other MCP clients can use take_screenshot, get_page_info, and capture_pdf.

Use the ScreenshotNeo API documentation for the complete option list, including full-page and element capture, device presets, custom CSS and JavaScript, waits, request blocking, authentication, geolocation, resizing, caching, signed links, asynchronous webhooks, bulk capture, and usage reporting.

cURL

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

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.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://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('node:fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

Practical checklist

  • Is the browser, viewport, scale factor, locale, and data fixed?
  • Does the wait express application readiness rather than an arbitrary delay?
  • Are animations, timestamps, ads, and third-party widgets controlled?
  • Does each checkpoint have a stable baseline name?
  • Are candidate and diff artifacts retained on failure?
  • Is there a documented threshold or mask policy?
  • Can a reviewer tell whether a baseline update was intentional?

Frequently Asked Questions

Can Selenium compare screenshots by itself?

Selenium captures screenshots but does not define your baseline repository, pixel threshold, diff artifact, or approval policy. Add a local image-comparison step or a visual-testing service.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I test every viewport?

Test the breakpoints and device widths that your product supports. Keep each viewport as a separate named checkpoint so a change at one breakpoint does not hide a failure at another.

Is a visual diff failure always a CSS bug?

No. Browser versions, fonts, data, animation, loading order, and third-party content can also change pixels. Reproduce the run with controlled inputs before changing CSS or accepting a baseline.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.