Use Laravel Browsershot to Screenshot a Webpage with Custom CSS Injected
Inject CSS into a webpage before capturing it with Laravel Browsershot. Get runnable PHP examples, timing tips, deployment guidance, and fixes for common errors.
Use Browsershot’s setOption('addStyleTag', ...) to inject CSS before capturing a webpage. Pass the CSS as the content of an addStyleTag option, then call save() to write an image file or screenshot() to get image bytes.
This guide uses the documented Browsershot v4 API. Check the Browsershot version installed in your Laravel project before applying it; package, Puppeteer, and Chrome setup can vary by project. See the Browsershot image guide and requirements.
1. Capture a webpage with injected CSS
In a Laravel service, controller, job, or command, construct a Browsershot instance for the URL, set the addStyleTag option, and save the image:
use Spatie\\Browsershot\\Browsershot;
$url = 'https://example.com';
$outputPath = storage_path('app/example-custom-style.png');
$css = <<<'CSS'
body {
font-size: 14px !important;
background: #f4f6f8 !important;
}
header,
footer,
.cookie-banner {
display: none !important;
}
CSS;
Browsershot::url($url)
->setOption('addStyleTag', json_encode([
'content' => $css,
], JSON_THROW_ON_ERROR))
->save($outputPath);
The CSS is inserted into the page as a style tag before the screenshot. !important can help override existing declarations, though specificity and inline styles still matter. Use selectors that match the target page, and avoid broad rules such as * { display: none } unless hiding everything is intentional.
Return the image bytes instead of writing a file
Use screenshot() when the caller needs the image data in memory, for example to return it from a Laravel response or pass it to storage code:
use Spatie\\Browsershot\\Browsershot;
$image = Browsershot::url('https://example.com')
->setOption('addStyleTag', json_encode([
'content' => 'body { font-size: 14px !important; }',
], JSON_THROW_ON_ERROR))
->screenshot();
return response($image)
->header('Content-Type', 'image/png');
Check your installed version’s output-format behavior and configure it as appropriate for the required format. Make sure the HTTP content type matches the actual image format.
Load CSS from a stylesheet URL
Puppeteer’s addStyleTag supports CSS content or a stylesheet URL. Browsershot passes this Puppeteer option through setOption. For a remote stylesheet, pass a url key instead of content:
Browsershot::url('https://example.com')
->setOption('addStyleTag', json_encode([
'url' => 'https://static.example.com/screenshot.css',
], JSON_THROW_ON_ERROR))
->save(storage_path('app/example.png'));
The browser must be able to fetch that stylesheet. A remote CSS request can fail because of network access, TLS, access controls, or the stylesheet host. Inline content avoids that extra dependency.
2. Choose when the page is ready
Injected CSS does not make a page’s JavaScript, fonts, images, or asynchronous data load sooner. If the screenshot is taken before the target content appears, wait for the condition that actually signals readiness. Browsershot documents delayed screenshots, waiting for a JavaScript function, and waiting for a selector in its image guide.
Wait for a selector
When a known element indicates that the page content is ready, wait for it before capture. For example, depending on your installed Browsershot version, use its documented selector-waiting method:
Browsershot::url('https://example.com/report')
->waitForSelector('.report-ready')
->setOption('addStyleTag', json_encode([
'content' => '.report-ready { outline: 0 !important; }',
], JSON_THROW_ON_ERROR))
->save(storage_path('app/report.png'));
Consult the method reference for the exact API supported by your installed package version. A selector wait is usually more reliable than guessing a fixed delay, but it can time out when the page never adds the selector.
Wait for JavaScript or a delay
If the page exposes a readiness condition, use a JavaScript-function wait supported by your installed version. If no reliable signal exists, use a bounded delay as a fallback and keep it as short as the page permits. Delays add time to every capture, while too little wait produces incomplete output.
For full-page captures, pages with lazy-loaded images may need scrolling or other page-specific preparation before capture. Confirm how your installed Browsershot version handles this and add the necessary readiness step for the site.
3. Use it in a Laravel endpoint or queued job
Controller example
A controller can write a generated image under Laravel’s storage directory and return a download or a response. Validate the URL source and keep capture work bounded; accepting arbitrary URLs from a public request is a security-sensitive design.
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Storage;
use Spatie\\Browsershot\\Browsershot;
class ScreenshotController
{
public function capture(Request $request)
{
$data = $request->validate([
'url' => ['required', 'url'],
]);
// In production, also restrict allowed hosts to a trusted list.
$path = 'screenshots/' . uniqid('shot-', true) . '.png';
$css = 'body { font-family: Arial, sans-serif !important; }';
$bytes = Browsershot::url($data['url'])
->setOption('addStyleTag', json_encode([
'content' => $css,
], JSON_THROW_ON_ERROR))
->screenshot();
Storage::disk('local')->put($path, $bytes);
return response()->download(storage_path('app/' . $path));
}
}
Validation with Laravel’s url rule checks URL syntax; it does not make fetching arbitrary hosts safe. Use an allowlist or other network restrictions appropriate to your application. Spatie explicitly warns that URLs and HTML passed to Browsershot must be validated and trusted; see its security documentation.
Queued work
Screenshot rendering can consume CPU and memory, so a Laravel queue is often a better fit for work that does not need to finish inside a web request. Keep the CSS string and capture inputs as job data, then create the Browsershot instance inside the job’s handler. Ensure the queue worker has the same Chrome, Node.js, and Puppeteer setup as the web or command environment that runs the capture.
If you use Spatie’s Laravel Screenshot wrapper rather than direct Browsershot, its withBrowsershot() method exposes the underlying instance for one-off customization. Its documentation notes that closures cannot be serialized for saveQueued(); see the wrapper documentation.
4. CSS and capture details to decide
| Need | Approach | Things to check |
|---|---|---|
| Apply a few screenshot-only overrides | Pass CSS using addStyleTag with content. |
Selector matching, specificity, inline styles, and whether page scripts later change the DOM. |
| Share a larger stylesheet | Use the url form of Puppeteer’s addStyleTag. |
Browser network access, TLS, stylesheet availability, and whether the URL is trusted. |
| Hide an element | Use a targeted selector such as .cookie-banner { display: none !important; }. |
Class names may change; hiding an overlay can expose content only if it was already loaded. |
| Capture after dynamic content | Wait for a selector or JavaScript readiness condition; use a delay only when needed. | Timeout bounds and behavior when the readiness signal never appears. |
| Save output | Call save($path). |
Directory permissions, path validity, and output format. |
| Use output in application code | Call screenshot() for image bytes. |
Memory use, response content type, and storage handling. |
Browsershot can work with a live URL or supplied HTML and renders through Puppeteer and headless Chrome. Use the URL approach for a real webpage; supplied HTML is useful when the markup itself is generated by your application. Be deliberate about external assets in supplied HTML, since they still require browser network access.
5. Install and deployment considerations
There is no universal installation command for every Laravel project: the needed Browsershot, Puppeteer, Node.js, and Chrome versions depend on the package version and deployment environment. Follow the requirements for the version you install, and make sure the actual PHP process running the capture can access the expected Node.js and browser binaries.
- Run captures in the same environment as production when validating deployment-specific browser paths, fonts, permissions, and network access.
- For containers and restricted environments, Chrome may need no-sandbox mode. The Laravel Screenshot guide documents global or per-screenshot configuration for environments that require it. Enable it only when required by that environment and understand the isolation tradeoff.
- Install fonts needed by the page or your injected CSS; missing fonts can change line wrapping and layout.
- Ensure the output directory is writable, and clean up generated files according to your retention needs.
- Set application and worker timeouts to allow for page navigation and rendering, while keeping limits bounded.
6. Performance, reliability, and cost
Each capture starts or uses a headless browser workflow and loads the target page. Capture time depends on page weight, scripts, network latency, readiness waits, and the rendering environment. Injecting a small inline stylesheet is usually a smaller dependency than fetching another remote stylesheet, but actual timing depends on the page and environment; the dossier provides no benchmark.
For reliability, prefer a page-specific readiness condition over an arbitrary long delay, set reasonable navigation and job timeouts supported by your installed version, and record failures with the URL, wait condition, and browser error. Retries can help with transient network failures, but repeated captures of a permanently inaccessible or malformed page will continue to fail. Reuse a queue worker or browser process only if your chosen integration supports it safely.
Browsershot itself is a package, not a per-screenshot hosted API plan; your costs come from the infrastructure and engineering needed to run PHP, Node.js, Chrome, storage, and any queue capacity. If you prefer a hosted screenshot API, ScreenshotNeo offers a GET request that returns an image or PDF. Its clean-capture behavior removes known consent platforms, newsletter popups, and chat widgets before capture; only clean shots are billed, while bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. See ScreenshotNeo for the service overview.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| CSS has no visible effect | The selector does not match, the style is overridden, or the capture happens before the target state exists. | Inspect the page’s actual selectors, increase specificity or use a targeted !important, and wait for the relevant page state. |
| JSON or option parsing error | The option is not encoded as the expected JSON object, or the CSS string contains invalid encoding. | Pass an object with content or url and encode with JSON_THROW_ON_ERROR so encoding failures are visible. |
| Stylesheet URL does not load | The headless browser cannot reach the host, the URL is invalid, TLS fails, or access is restricted. | Check the URL from the capture environment; use inline CSS where practical and ensure any remote asset is trusted and reachable. |
| Screenshot is blank or incomplete | Navigation failed, JavaScript content is late, a selector wait is wrong, or the page returned a bot check or error page. | Check navigation and console errors, wait for a real readiness condition, and verify the target URL in the same network environment. |
| Images or fonts are missing | Assets have not loaded, are blocked, or are inaccessible to the browser. | Check asset requests and credentials, wait for relevant content, and install required local fonts in the runtime. |
| Chrome fails in a container | Browser dependencies, executable paths, permissions, or sandbox constraints differ from local development. | Follow the installed Browsershot version’s requirements, confirm binary paths and libraries, and configure no-sandbox only when the environment requires it. |
| Output file cannot be written | The directory is missing or the PHP worker lacks permission. | Use a valid absolute path, create the directory, and grant the runtime user appropriate write access. |
| Capture times out | The page never reaches the requested condition or navigation is slow. | Use a condition that exists on that page, bound delays and waits, and adjust supported timeouts to the workload. |
8. Security checklist
- Only capture URLs and HTML that your application trusts. Validate inputs and restrict allowed hosts where users can submit URLs.
- Consider server-side request forgery: a headless browser may reach internal services or local addresses unless network access is restricted.
- Do not put secrets in injected CSS or expose authenticated cookies and headers to arbitrary pages.
- Keep browser and package dependencies maintained, and run rendering workers with only the filesystem and network access they need.
Or skip the browser setup
ScreenshotNeo can return a screenshot with one GET request; see the API documentation. For example, this cURL command saves a WebP image:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.
9. FAQ
Can I inject CSS into HTML instead of a live URL?
Yes. Browsershot supports supplied HTML as well as URLs. Use the input method documented for your installed version, then apply the same addStyleTag option before capture.
Does injected CSS change the website for other visitors?
No. It is applied in the browser session used for that capture. It does not publish changes to the site’s source files.
Should I use Browsershot or Laravel Dusk?
Use Browsershot for generating webpage images or PDFs in application workflows. Use Dusk screenshots when you need browser-test evidence; Dusk also supports capturing a specific element. See the Laravel Dusk documentation.
Can I inject JavaScript too?
Browsershot supports browser automation options beyond styles. Choose the documented method for your installed version and run only trusted scripts against trusted pages; this guide’s CSS injection uses Puppeteer’s style-tag option.


