ScreenshotNeo

BlogHow-to

How to Replace Images in Automated Website Screenshots

Replace images in Playwright or Puppeteer screenshots with CSS, DOM changes, or intercepted network responses—and keep visual tests stable.

By the ScreenshotNeo team29 September 202610 min read

How to Replace Images in Automated Website Screenshots

To replace an image in an automated website screenshot, choose the layer that matches the job: use a screenshot-only CSS override or change the page’s DOM when the element already exists; intercept its network request when the page must receive different image bytes or the image is created dynamically. Apply the change before capture, wait for the replacement to load and decode, and hold the browser environment steady for repeatable output.

This guide covers Playwright and Puppeteer, including complete examples, synchronization, common failure modes, and tradeoffs. The examples use local fixture files so the visual test does not depend on an unstable remote image host.

1. Choose the replacement method

Situation Use Why
An existing <img> or CSS background needs a visual substitute DOM or CSS override Simple, local to the capture, and can preserve the existing box and layout.
The page must receive different image bytes Network interception The browser renders your fixture as the response to the image request.
Images are created dynamically, come from third parties, or use changing URLs URL or resource-type interception Intercepting requests can cover images requested after initial markup.
You need to omit an image Hide it with CSS, or abort its request CSS keeps the request behavior intact; aborting prevents the resource from loading.

For a one-off screenshot or a fixed visual regression fixture, CSS is usually the lightest option. Choose interception if replacing the pixels only at presentation time is insufficient—for example, if the application reads image dimensions or pixels, or if the resource is inserted after navigation. When layout matters, match the original image’s aspect ratio and sizing rules.

2. Playwright: apply screenshot-only CSS

Playwright screenshot assertions support a stylesheet that applies only while taking the screenshot. This is useful when the page should remain untouched during the test, and the override only needs to affect rendered output. The style is applied through Shadow DOM and inner frames as well as ordinary page content. See the Playwright screenshot assertion documentation.

import { test, expect } from '@playwright/test';

 test('hero image uses a stable visual fixture', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('page.png', {
    stylePath: 'tests/fixtures/screenshot-overrides.css',
    animations: 'disabled',
    fullPage: true,
  });
});
/* tests/fixtures/screenshot-overrides.css */
img.hero {
  visibility: hidden;
}

.hero-frame {
  background: url("../fixtures/replacement.png") center / cover no-repeat;
}

For this pattern, wrap the image in a stable container such as .hero-frame. Hiding the image and painting the fixture on its container avoids relying on replacing an image source through CSS. Adjust the selectors and relative asset path to the application. If hiding the original would collapse the layout, use visibility: hidden rather than display: none, or preserve the box with an explicit aspect ratio and dimensions.

When you need a true source swap, change the DOM and wait for decoding before capture:

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });

try {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.locator('img.hero').evaluate(async (img) => {
    img.src = 'file:///absolute/path/to/replacement.png';
    if (!img.complete) {
      await new Promise((resolve, reject) => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', reject, { once: true });
      });
    }
    if (img.decode) await img.decode();
  });
  await page.screenshot({ path: 'page.png', fullPage: true, animations: 'disabled' });
} finally {
  await browser.close();
}

Use an absolute file URL for a local fixture; in a project, build it with pathToFileURL(resolve(...)).href rather than assuming the current working directory. If the page’s content security policy, browser context, or application code makes a local URL unsuitable, serve the fixture from a local test server or use route fulfillment below. Changing img.src does not replace a CSS background; update the relevant element’s style.backgroundImage or use a stylesheet override.

3. Playwright: replace image responses with a route

Register routes before navigating so early image requests are included. The handler must pass every unrelated request through. Playwright’s page routing API lets a route fulfill a request with a file, body, or other response data.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

try {
  await page.route('**/*', async (route) => {
    const request = route.request();
    if (request.resourceType() === 'image' && request.url().includes('/hero')) {
      await route.fulfill({
        path: 'tests/fixtures/replacement.png',
        contentType: 'image/png',
      });
      return;
    }
    await route.continue();
  });

  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  await page.screenshot({ path: 'page.png', fullPage: true, animations: 'disabled' });
} finally {
  await browser.close();
}

The URL condition is deliberately narrow: intercepting every image can accidentally replace icons, logos, and product photos that the screenshot is meant to test. If the request URL varies but the target is known, match a path fragment or regular expression. For a dynamically created image, install the route before the code that causes it to be created.

Service workers can intercept requests before page routes see them. If interception appears to be ignored, create the context with serviceWorkers: 'block' when compatible with the test. That changes service-worker behavior, so use it only when the test is not specifically validating the worker.

4. Puppeteer: respond to image requests

Puppeteer request interception exposes the request’s resource type and lets the handler respond with fixture bytes. Once interception is enabled, every request stalls until it is continued, responded to, or aborted. That includes scripts, stylesheets, fonts, documents, and requests unrelated to the target image. See the Puppeteer network logging guide and HTTPRequest API.

Network interception supplies a fixture image before the browser renders the page.
Network interception supplies a fixture image before the browser renders the page.
import puppeteer from 'puppeteer';
import { readFile } from 'node:fs/promises';

const replacementPngBuffer = await readFile('tests/fixtures/replacement.png');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

try {
  await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
  await page.setRequestInterception(true);
  page.on('request', async (request) => {
    try {
      if (request.resourceType() === 'image' && request.url().includes('/hero')) {
        await request.respond({
          status: 200,
          contentType: 'image/png',
          body: replacementPngBuffer,
        });
      } else {
        await request.continue();
      }
    } catch (error) {
      if (!request.isInterceptResolutionHandled()) {
        await request.continue().catch(() => {});
      }
    }
  });

  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

Every intercepted request must be resolved exactly once. In code that can register multiple handlers or use cooperative interception, check the request’s resolution state and ensure no competing handler responds first. Keep the handler small and avoid doing slow file reads for every request; load fixture buffers before enabling interception. Use a narrow URL check alongside resourceType() === 'image' when only one asset should change.

5. Make the capture deterministic

A replacement can be correct while the screenshot still varies. Control both image readiness and the rendering environment.

  1. Install the override before the relevant request. Register routes before navigation. For DOM replacement, wait until the target element exists, set its source, then wait for load and decode().
  2. Wait for layout to settle. Lazy images may not be requested until scrolled into view. Scroll the target into view or capture the full page, and wait for any application-specific layout update before capturing.
  3. Disable motion. Playwright screenshot options can disable animations. For Puppeteer, inject test CSS such as * { animation: none !important; transition: none !important; caret-color: transparent !important; } before capture.
  4. Pin the rendering setup. Keep browser version, operating system, viewport, device scale factor, fonts, color scheme, and locale consistent. Playwright notes that rendering can vary with host OS, browser version, hardware, power source, headless mode, and other environment details; see its visual comparisons guide.
  5. Use the right dimensions. Use a full-page screenshot when the target can be below the viewport. Set viewport and device scale factor explicitly so CSS pixels and output pixels are predictable.

Network-idle waits are not a universal definition of visual readiness: polling, analytics, and long-lived connections can keep a page active, while an image may still need decoding after its response arrives. Prefer waiting for the target image’s decoded state or a page-specific ready signal. For screenshot assertions, Playwright’s comparison flow also waits for stable consecutive screenshots and disables animations by default; that helps with transient differences but does not make remote content deterministic.

6. Edge cases and tradeoffs

CSS backgrounds and responsive images

An <img> source swap does not affect background-image, srcset, or <picture> source selection. For CSS backgrounds, override the property on the element. For responsive images, set src and clear or update srcset and sizes, or intercept the selected request. Verify the browser selected the fixture URL when using network routing.

A local visual override keeps the capture independent of changing remote image URLs.
A local visual override keeps the capture independent of changing remote image URLs.

Cross-origin and cached resources

DOM replacement with a remote fixture can be subject to the page’s security policy and cross-origin restrictions, particularly if the test reads image pixels. For screenshots, routing the response avoids depending on the remote host. A cached response can make it look as if an override did not run; install interception before navigation and consider a fresh browser context for each isolated test.

Image dimensions and layout shifts

A fixture with a different aspect ratio can change the page height, shift surrounding content, or change a full-page capture’s dimensions. Prefer matching the original intrinsic dimensions, or set explicit width, height, aspect-ratio, and object-fit in the test stylesheet.

Animated formats and transparency

GIF, animated WebP, and video-like image content can capture at different frames unless time is controlled or the asset is replaced with a static fixture. Preserve alpha when transparency is part of the design; changing from a transparent PNG to an opaque image changes the rendered background as well as the asset.

Suppressing instead of replacing

To hide an image visually, use CSS and retain its layout box where necessary. To prevent a request, abort it through interception; handle the page’s broken-image behavior if the element remains visible. A route fulfilled with an empty or malformed body is not equivalent to a clean visual hide.

7. Troubleshooting

Symptom Likely cause Fix
Original image still appears Override installed after request; selector or URL match is wrong; service worker or cache supplied it. Register route before navigation, inspect the exact request URL, narrow-check the selector, and test in a fresh context. Block service workers only if appropriate.
Broken-image icon or blank fixture Bad fixture path, incorrect MIME type, unreadable file, or invalid image bytes. Resolve the path from a known project directory, use matching contentType, and verify the fixture file and format.
Puppeteer navigation hangs An intercepted request was never resolved or a request handler errored. Ensure every request is continued, responded to, or aborted exactly once; log failures and keep fixture reads out of the handler.
Screenshot captures old pixels Source changed, but decoding or layout had not completed. Wait for the image’s load and decode promises, then wait for the relevant layout or application ready signal.
Screenshot dimensions change Fixture dimensions differ, a lazy image changes layout, or viewport/full-page behavior differs. Match intrinsic dimensions, set aspect ratio and viewport explicitly, and wait for lazy content.
Only some responsive viewports replace the image srcset, picture, or media queries select a different URL or CSS rule. Intercept all candidate URLs for that element or update its responsive sources and test each viewport.
Unrelated images changed Route matches all requests of image resource type. Add a hostname, path, or URL predicate so only the intended asset is fulfilled.

8. Performance, reliability, and cost

CSS and DOM overrides are generally the least involved: they avoid replacing unrelated requests and keep the fixture local to the capture. Network interception adds a handler to every matching request, so keep the match narrow and fixture bytes ready in memory. Avoid waiting for the entire network when a specific image decode or application signal is sufficient.

For reliable visual regression, commit small deterministic fixtures, pin the browser environment, and avoid expiring remote image URLs. A screenshot run’s cost depends on the infrastructure running the browser, including machine time and any remote browser service used; the research sources provide no universal benchmark or price. Measure in the project’s own CI rather than assuming a fixed speed difference. If a remote screenshot API is preferable to maintaining browser setup, compare its pricing and capture behavior before routing a test suite through it.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, and its screenshot options include custom CSS and JavaScript, selector waits, full-page capture, viewport controls, and device presets. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots, and all features are on every plan.

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

10. FAQ

Can I replace images without changing the application source?

Yes. Use screenshot-only CSS or browser request routing in the test harness. Neither requires modifying the app’s production files.

Should I replace every image in a visual test?

Only if remote assets are outside the behavior under test. Keep real assets when their appearance is part of the feature you are validating.

Is an image response replacement the same as changing an image after page load?

No. Interception supplies fixture bytes to a request; a DOM change updates the element after the page has loaded. Use interception when request timing or dynamic insertion matters.

How do I test replacement behavior at multiple screen sizes?

Run the same fixture with explicit viewport and device scale settings for each target size. Responsive sources and CSS media rules can select different assets at each viewport.

Primary references