ScreenshotNeo

BlogHow-to

How to Take Cross-Browser Webpage Screenshots with PHP

Learn how to capture consistent webpage screenshots in Chromium, Firefox, and WebKit from PHP, with full-page, element, and CI-ready examples.

By the ScreenshotNeo team1 October 20268 min read

PHP does not render webpages itself; it controls a browser engine that renders the page and saves an image. For Chrome-only captures, Spatie Browsershot provides a concise PHP wrapper around Puppeteer and headless Chrome. For Chromium, Firefox, and WebKit coverage, use the Playwright PHP project or invoke Playwright from PHP, then run the same capture specification against each engine.

A Chrome wrapper is not cross-browser coverage. Browsershot controls headless Chrome, so it cannot establish Firefox or WebKit rendering. The Playwright PHP project lists Chromium, Firefox, and WebKit support and requires PHP 8.2 or newer, but its documentation is still under review; verify its current API and installation steps before adopting it.

1. Define what “cross-browser” means

Decide which dimension you need to compare:

  • Engines: Chromium, Firefox, and WebKit.
  • Browser versions: a pinned version in CI versus a locally installed version.
  • Operating systems: Linux, macOS, and Windows can render fonts and native controls differently.

The Playwright PHP project establishes engine scope, not a complete browser-version and operating-system matrix. Treat each engine and environment as a separate capture target.

2. Capture a Chrome screenshot with Browsershot

Use Browsershot when Chrome is the target engine. Its documented API supports URL capture, full-page screenshots, viewport dimensions, device emulation, delayed capture, selector waits, JavaScript waits, and image output.

Install

composer require spatie/browsershot
npm install puppeteer

Install a compatible Chrome or Chromium runtime in the machine, container, or CI image where the script runs. Check the Browsershot documentation for version-specific requirements.

Minimal PHP script

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com')
    ->windowSize(1440, 900)
    ->save(__DIR__ . '/example-chrome.png');

Full-page, delayed, and selector-aware capture

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com/dashboard')
    ->windowSize(1440, 900)
    ->fullPage()
    ->waitUntilNetworkIdle()
    ->waitForSelector('#report-ready')
    ->delay(500)
    ->save(__DIR__ . '/dashboard-full.png');

Use a selector that represents the content you actually need. Network idle alone may occur before an application finishes rendering data.

Element screenshot

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com/pricing')
    ->windowSize(1440, 900)
    ->select('.pricing-table')
    ->save(__DIR__ . '/pricing-table.png');

3. Run Chromium, Firefox, and WebKit from PHP

For engine comparison, Playwright is the practical fit. The PHP project describes a typed PHP API for Chromium, Firefox, and WebKit, with PHP 8.2 or newer. Because that project’s documentation is under review, confirm the package name, namespace, browser-install command, and method names against its current site before locking your dependency.

A stable integration pattern is to install Playwright and call its CLI from PHP. This keeps the engine choice explicit and lets you pin the Playwright version in your project.

Install Playwright browsers

npm install -D playwright
npx playwright install chromium firefox webkit

PHP wrapper around the Playwright CLI

<?php

function captureWithPlaywright(string $browser, string $url, string $output): void
{
    $allowed = ['chromium', 'firefox', 'webkit'];
    if (!in_array($browser, $allowed, true)) {
        throw new InvalidArgumentException('Browser must be chromium, firefox, or webkit.');
    }

    $command = [
        'npx', 'playwright', 'screenshot',
        '--browser=' . $browser,
        '--device-scale-factor=1',
        '--viewport-size=1440,900',
        '--full-page',
        $url,
        $output,
    ];

    $escaped = array_map('escapeshellarg', $command);
    $process = proc_open(
        implode(' ', $escaped),
        [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
        $pipes
    );

    if (!is_resource($process)) {
        throw new RuntimeException('Could not start Playwright.');
    }

    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    $exitCode = proc_close($process);

    if ($exitCode !== 0) {
        throw new RuntimeException("Playwright failed for {$browser}: {$stderr}{$stdout}");
    }
}

$url = 'https://example.com';
foreach (['chromium', 'firefox', 'webkit'] as $browser) {
    captureWithPlaywright($browser, $url, __DIR__ . "/shot-{$browser}.png");
}

This CLI example captures the same URL, viewport, scale, and full-page scope in all three engines. For application-level control such as waiting for a selector, injecting CSS, setting cookies, or selecting an element, use the current Playwright PHP API after verifying its documentation and feature parity.

4. Make captures comparable

  1. Fix the viewport: use the same width and height for every engine.
  2. Fix device scale: use one scale factor when comparing pixels.
  3. Choose the scope: visible viewport, one element, or the full scrollable page.
  4. Wait for application state: wait for a meaningful selector or app-specific ready flag.
  5. Freeze dynamic content: disable animations, clocks, rotating banners, random IDs, ads, and live counters.
  6. Control fonts: install the same font files or accept that font rasterization can differ by operating system.
  7. Use stable data: seed test records and keep authentication state isolated.
  8. Choose a format deliberately: PNG is the safest default for pixel comparisons; JPEG reduces size but introduces lossy differences. WebP support depends on the PHP binding and browser tooling you select.

5. Full-page versus viewport versus element screenshots

Scope Use it for Typical issue
Viewport Visual regression of the first screen or responsive layouts Content below the fold is omitted
Element Cards, invoices, charts, or components Selector may be hidden or move after hydration
Full page Documentation, landing pages, and long reports Lazy images and sticky elements need special handling

6. Authentication, headers, cookies, and private pages

Keep credentials outside source control and screenshot artifacts. In CI, inject short-lived secrets through the job’s secret store. Confirm that the same cookies, authorization headers, locale, and timezone are applied to every engine; otherwise a visual difference may be an authentication or localization difference rather than an engine difference.

7. Common errors and fixes

Error Cause Fix
Class "Spatie\\Browsershot\\Browsershot" not found Composer autoloader or dependency is missing Run composer require spatie/browsershot and include vendor/autoload.php.
Chrome or Chromium executable not found Browser binary is absent or the process cannot see it Install the browser in the runtime image and configure the executable path according to Browsershot’s version-specific documentation.
npx playwright install succeeds locally but fails in CI Browser binaries are not cached or the CI image lacks required system libraries Install browsers during image build, cache the Playwright directory, and use a supported base image.
Blank or incomplete screenshot Capture occurred before hydration, lazy loading, or client data finished Wait for a content selector or application-ready condition; add a short delay only when the page has no reliable signal.
Different screenshots on each run Animations, timestamps, ads, random data, or fonts change Freeze time and data, disable animation, block unstable resources, and standardize fonts.
Full-page image cuts off content Nested scroll container or virtualized list Capture the relevant element, expand the scroll container, or use an application state that renders all rows.
Firefox/WebKit output differs dramatically Engine-specific CSS, missing font, unsupported feature, or setup mismatch Check console errors, computed styles, fonts, viewport, locale, and browser versions before treating it as a rendering defect.

8. Performance, reliability, and cost

  • Launching a browser for every URL is expensive. Reuse a browser process when your integration permits it, while creating isolated contexts for cookies and authentication.
  • Parallelize independent URLs carefully. Limit concurrency to the CPU and memory available in the worker; too many engines at once can cause timeouts.
  • Cache browser binaries in CI and pin package versions so a browser update does not silently change baselines.
  • Use a deterministic URL and output naming scheme, such as {page}-{engine}-{viewport}.png.
  • Store failure logs and the HTML URL alongside the image, but redact credentials and private query parameters.
  • The reviewed documentation does not provide a directly comparable speed or cost benchmark for Browsershot and Playwright PHP. Measure your own pages, engines, and CI hardware.

9. Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API when you do not want to install and maintain browser binaries. One GET request returns PNG, JPEG, WebP, or PDF output. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.

cURL

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

PHP

<?php

$params = http_build_query([
    'access_key' => 'YOUR_API_KEY',
    'url' => 'https://stripe.com',
]);

$context = stream_context_create([
    'http' => ['timeout' => 90],
]);

$image = file_get_contents(
    "https://api.screenshotneo.com/v1/shot?{$params}",
    false,
    $context
);

if ($image === false) {
    throw new RuntimeException('ScreenshotNeo request failed.');
}

file_put_contents(__DIR__ . '/shot.webp', $image);

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

See the ScreenshotNeo API documentation for the full option set: full-page or CSS-element capture, dark mode, device presets and custom viewports, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and usage data.

Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Responses include X-Page-Verdict and X-Billed headers so your application can see what happened. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots.

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

10. FAQ

Can PHP take a screenshot in Firefox and Safari?

PHP can control browser engines through a library or process. The Playwright PHP project lists Chromium, Firefox, and WebKit. WebKit is the engine named by that project; do not treat this as a guarantee for every Safari version or operating system.

Does Browsershot support browsers other than Chrome?

Its documented backend is Puppeteer controlling headless Chrome. Use it for Chrome screenshots, not as evidence of Firefox or WebKit coverage.

Should visual tests use PNG or JPEG?

Use PNG when pixel-level comparison matters. JPEG can reduce file size but adds compression differences. Confirm WebP support in the exact PHP integration you deploy.

Is a full-page screenshot always better?

No. Use viewport captures for responsive first-screen checks and element captures for components. Full-page captures are useful for long documents but require care with lazy loading and sticky elements.

How do I prevent false visual diffs?

Keep viewport, scale, fonts, locale, timezone, data, authentication, animations, ads, and browser versions consistent. Wait for a meaningful ready condition before capturing.