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

Use PHP to control a real browser, then pass an explicit filesystem path to the browser’s screenshot method. PHP cannot reliably rasterize an arbitrary, JavaScript-heavy webpage by itself. Playwright or Puppeteer renders the page, waits for it to reach the state you need, and writes PNG, JPEG, or WebP bytes to a folder you control.

The shortest Playwright-style operation is:

$page->goto('https://example.com');
$page->screenshot(__DIR__ . '/screenshots/page.png');

This guide shows a complete PHP workflow, a Puppeteer worker option, full-page and element captures, safe filenames, permissions, concurrency, troubleshooting, and a browser-free alternative.

What PHP needs to save a webpage screenshot

A screenshot has two distinct parts: rendering and storage. A browser engine performs the rendering; PHP supplies the URL, timing and destination path. Your application therefore needs:

  • A browser automation library such as Playwright or Puppeteer, plus its browser runtime.
  • A destination directory that exists and is writable by the PHP-FPM, web-server or queue-worker user.
  • An explicit filename with an image extension such as .png, .jpg or .webp.
  • Timeout and cleanup rules appropriate for your workload.

Keep captures containing private or customer data outside a public web root unless public access is intentional. Build paths from __DIR__ or a configured storage root rather than the process’s current working directory; workers and cron jobs may start elsewhere.

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

Option 1: Playwright PHP

Playwright PHP exposes a screenshot method whose first argument is the destination path. A minimal capture, after your application has created a page object, is:

$page->goto('https://example.com');
$page->screenshot(__DIR__ . '/artifacts/home.png');

The method returns image data as a string as well, but supplying a path writes the file directly. Use an absolute path and create the directory before navigation.

A production-oriented capture function

<?php

function saveScreenshot($page, string $url, string $storageRoot, string $name): string
{
    $directory = rtrim($storageRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'screenshots';

    if (!is_dir($directory) && !mkdir($directory, 0750, true) && !is_dir($directory)) {
        throw new RuntimeException("Cannot create screenshot directory: {$directory}");
    }
    if (!is_writable($directory)) {
        throw new RuntimeException("Screenshot directory is not writable: {$directory}");
    }

    // Permit a simple application-generated name; do not accept arbitrary path input.
    $safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
    if ($safeName === '' || pathinfo($safeName, PATHINFO_EXTENSION) === '') {
        $safeName .= '.png';
    }
    $path = $directory . DIRECTORY_SEPARATOR . $safeName;

    $page->goto($url, ['waitUntil' => 'networkidle', 'timeout' => 60000]);
    $page->screenshot($path, [
        'fullPage' => true,
        'type' => 'png'
    ]);

    if (!is_file($path) || filesize($path) === 0) {
        throw new RuntimeException('The browser returned without creating a non-empty image.');
    }
    return $path;
}

// $page is a page created by your Playwright bootstrap.
$path = saveScreenshot($page, 'https://example.com', __DIR__ . '/var', 'example-' . date('Ymd-His') . '.png');
echo "Saved to {$path}n";

The exact browser-launch code depends on the Playwright PHP package and version you have installed. Keep browser startup and shutdown in your application bootstrap, and always close the browser or release the context in a finally block.

Choose the capture scope

Need Setting Result
What a visitor currently sees Default screenshot The current viewport only
The complete scrollable document fullPage => true A tall image containing the page below the fold
One card, chart or component Locate an element and call its screenshot method Only that element’s bounding box

For an element capture, use your library’s locator API rather than guessing coordinates. Conceptually:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
$page->locator('.invoice-summary')->screenshot(__DIR__ . '/artifacts/summary.png');

Selectors must match the rendered DOM. If a component is created after navigation, wait for it explicitly before taking the image.

Option 2: PHP orchestrating a Puppeteer worker

Puppeteer’s page.screenshot() accepts a path option. The extension determines the image type, and relative paths are resolved against the Node process’s current working directory, so an absolute path is safer.

Node worker

// capture.js
const puppeteer = require('puppeteer');

(async () => {
  const [url, output] = process.argv.slice(2);
  if (!url || !output) throw new Error('Usage: node capture.js URL ABSOLUTE_OUTPUT_PATH');

  const browser = await puppeteer.launch({headless: true});
  try {
    const page = await browser.newPage();
    await page.setViewport({width: 1440, height: 900, deviceScaleFactor: 1});
    await page.goto(url, {waitUntil: 'networkidle2', timeout: 60000});
    await page.screenshot({path: output, fullPage: true});
  } finally {
    await browser.close();
  }
})();

PHP wrapper

<?php

$url = 'https://example.com';
$directory = __DIR__ . '/screenshots';
if (!is_dir($directory) && !mkdir($directory, 0750, true) && !is_dir($directory)) {
    throw new RuntimeException('Could not create screenshot directory');
}
if (!is_writable($directory)) {
    throw new RuntimeException('Screenshot directory is not writable');
}

$output = $directory . '/example-' . date('Ymd-His') . '.png';
$command = 'node ' . escapeshellarg(__DIR__ . '/capture.js') . ' '
         . escapeshellarg($url) . ' ' . escapeshellarg($output) . ' 2>&1';
exec($command, $lines, $status);
if ($status !== 0 || !is_file($output) || filesize($output) === 0) {
    throw new RuntimeException("Capture failed (status {$status}): " . implode("n", $lines));
}
echo "Saved to {$output}n";

Install Puppeteer in the directory containing capture.js, install its browser runtime, and run the worker under the same user that owns the destination directory. In a web request, a queue worker is usually more reliable than waiting for a browser launch to finish synchronously.

Wait for the page you actually want

networkidle is useful for mostly static pages, but analytics, advertisements and live connections can prevent true idleness. Choose a condition that represents the content you need:

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.
  • Navigation completion: use the library’s load or DOM-content-loaded state for simple documents.
  • A selector: wait until .report-table or another required component is visible.
  • A fixed delay: reserve this for animations or third-party widgets with no dependable selector.
  • Application readiness: expose a page flag or wait for a known API result when your own site controls the code.

Disable animations or add a small, deliberate delay when visual comparisons matter. Fonts, viewport size, device scale, browser version, timezone and geolocation all affect pixels. A screenshot is visual evidence, not a substitute for normal DOM or accessibility assertions.

Folders, names and concurrent jobs

Permissions and web visibility

The directory must be writable by the effective PHP or worker user, not merely by your login account. A permission failure commonly occurs after deployment when ownership changes. Store sensitive images in a private directory and stream them through an authorization check instead of placing them under public/.

Collision-resistant names

Use a database ID, UUID or timestamp plus a random suffix. Never concatenate an untrusted URL directly into a path, and reject path separators even after sanitizing. If two jobs can capture the same record, write to a temporary filename and rename after a successful, non-empty capture.

Retention

Screenshot directories grow without bound unless you remove old files. A scheduled cleanup can delete files older than a chosen age or keep only a maximum count per project. Do not delete a file while another process is still uploading it; atomic rename and a job status record make this easier to coordinate.

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.

Common failures and precise fixes

Symptom Likely cause Fix
“No such file or directory” The folder was never created, or a relative path resolved elsewhere. Create it recursively and use __DIR__ or an absolute storage root.
Permission denied PHP-FPM or the worker cannot write the directory. Correct ownership and mode for that service user; verify with is_writable().
Empty or missing image The browser failed before writing, or the process was terminated. Capture stderr, check the exit status, increase the navigation timeout and verify the browser runtime.
Cookie banner, popup or spinner in the image The page was captured before interaction or before the UI settled. Wait for the required selector, click or hide the element, and disable animations where appropriate.
Only the top portion appears A viewport screenshot was requested. Enable full-page capture, or capture the specific element you need.
Page is blank or partially rendered JavaScript error, blocked resource, bot challenge or an overly short wait. Inspect console/network logs, test the URL in the same browser environment and wait for a meaningful readiness selector.
Works in a shell but not through PHP Different PATH, user, environment variables or working directory. Use absolute executable paths, log the effective user and pass absolute output paths.
Images differ between runs Fonts, animations, responsive viewport, time or data changed. Pin viewport and browser version, wait for fonts/data, freeze time where possible and compare under the same environment.

Performance, reliability and cost decisions

Launching a browser for every request is expensive. Keep a browser process alive in a worker, create an isolated context per job, and close pages promptly. Limit concurrency according to available CPU and memory; a queue prevents web requests from timing out while large full-page captures run.

Set separate navigation and overall-job timeouts. Record the URL, viewport, browser version, elapsed time, output path and failure reason. Retry transient navigation failures with a small backoff, but do not retry deterministic HTTP errors indefinitely. If a destination is user-supplied, restrict schemes to HTTPS and HTTP, block internal network ranges as appropriate for your threat model, and cap image dimensions and job duration.

Use PNG for lossless text and UI evidence, JPEG for photographic pages when size matters, and WebP when your downstream system supports it. Full-page images consume more memory than viewport captures; element screenshots are often the cheapest option for dashboards or product cards.

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

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP or PDF, so PHP can save the response directly without installing a local browser:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for parameters and response headers. A PHP equivalent using cURL is:

<?php
$url = 'https://stripe.com';
$output = __DIR__ . '/screenshots/stripe.webp';

if (!is_dir(dirname($output))) {
    mkdir(dirname($output), 0750, true);
}
$ch = curl_init('https://api.screenshotneo.com/v1/shot?' . http_build_query([
    'access_key' => 'YOUR_API_KEY',
    'url' => $url,
]));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT => 90,
]);
$bytes = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($bytes === false || $status < 200 || $status >= 300) {
    throw new RuntimeException("Screenshot request failed ({$status}): {$error}");
}
file_put_contents($output, $bytes, LOCK_EX);

ScreenshotNeo removes cookie and consent banners, newsletter popups and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. Features include full-page and CSS-selector captures, device presets, retina scale, PDF controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.

FAQ

Can PHP take a screenshot without JavaScript?

It can save an image returned by a service, but a local capture of a modern webpage still needs a rendering engine. PHP’s role is orchestration and file handling.

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

Which extension should I use?

Use PNG for crisp interfaces and text, JPEG for smaller photographic images, and WebP when your consumer supports it. Match the extension to the format requested from the browser or API.

Why is an absolute path safer?

Relative paths depend on the process working directory, which can differ between a web request, CLI command and queue worker. An absolute path based on a known storage root removes that ambiguity.

Frequently Asked Questions

Can PHP take a screenshot without JavaScript?

It can save an image returned by a service, but a local capture of a modern webpage still needs a rendering engine. PHP’s role is orchestration and file handling.

Which extension should I use?

Use PNG for crisp interfaces and text, JPEG for smaller photographic images, and WebP when your consumer supports it. Match the extension to the format requested from the browser or API.

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

Why is an absolute path safer?

Relative paths depend on the process working directory, which can differ between a web request, CLI command and queue worker. An absolute path based on a known storage root removes that ambiguity.

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.