ScreenshotNeo

BlogAI agents

How to Capture a Webpage Screenshot with an AI Agent and Custom CSS

Use Playwright to let an AI agent inspect a webpage, apply custom CSS, and save a screenshot. Choose between capture-only styles and persistent stylesheet injection.

By the ScreenshotNeo team4 October 20269 min read

Use Playwright as the browser automation layer: navigate to the page, let your agent inspect or interact with it, apply CSS either just for the screenshot or to the live page, and capture the result. The screenshot API’s style option is best when CSS should affect only the image; page.addStyleTag() is better when the agent should also see and interact with the styled page.

Playwright provides browser and page APIs, including an AI-oriented ARIA snapshot format, but it does not prescribe an AI model or agent framework. The agent is your orchestration layer: it decides what to inspect, which CSS to apply, and when to capture.

1. Install Playwright and prepare a page

This runnable Node.js example starts Chromium, opens a page, applies a stylesheet only while capturing, and saves a PNG. It uses a fixed viewport to make the output easier to reproduce.

import { chromium } from 'playwright';

const url = 'https://example.com';
const css = `
  .cookie-banner, .newsletter-modal, .chat-widget { display: none !important; }
  body { font-family: Arial, sans-serif !important; }
`;

const browser = await chromium.launch({ headless: true });
try {
  const context = await browser.newContext({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
  });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
  await page.screenshot({ path: 'screenshot.png', style: css, fullPage: true });
} finally {
  await browser.close();
}

Install the package with npm install playwright and install its browser with npx playwright install chromium. Save the example as an ES module, such as screenshot.mjs, then run node screenshot.mjs. Replace the example URL and CSS selectors with ones appropriate for the page. A selector that does not match simply has no effect.

2. Let the agent inspect and decide

Before choosing a target or writing CSS, an agent can inspect the page’s accessible structure. Playwright’s AI mode returns an ARIA snapshot with element references that can help an agent reason about controls and page content.

const snapshot = await page.ariaSnapshot({ mode: 'ai' });
console.log(snapshot);
// Give the snapshot to your agent, then use its decision to interact with the page.

The snapshot is input to your agent; it does not itself choose a model, generate CSS, or capture an image. Keep the agent’s instructions bounded: identify the content to capture, name transient elements to hide, and return CSS plus a capture scope. Validate generated selectors before relying on them.

3. Choose how the stylesheet is applied

Capture-only CSS

Pass CSS through page.screenshot({ style }) when the styling should be temporary and limited to that screenshot. This is useful for hiding a popup, suppressing animations, or making a one-off visual adjustment without changing the page state used for subsequent agent actions.

await page.screenshot({
  path: 'capture-only.png',
  style: '.announcement, .chat-widget { visibility: hidden !important; }',
});

Persistent page CSS

Use page.addStyleTag() when the agent needs to inspect or interact with the styled page after the stylesheet is applied. Playwright accepts stylesheet content, a local path, or a URL.

await page.addStyleTag({
  content: '.announcement, .chat-widget { display: none !important; }',
});
// Later screenshots and page interactions see the injected stylesheet.

For a file-based stylesheet, use await page.addStyleTag({ path: './capture.css' }). A URL form is also available: await page.addStyleTag({ url: 'https://example.com/capture.css' }). Use a local file when the stylesheet is part of your project and version controlled. Only use a stylesheet URL you trust and can access from the browser environment.

Method CSS lifetime Use it when
screenshot({ style }) Capture operation Only the saved image should change.
addStyleTag({ content }) Page lifetime Inspection, interactions, and later screenshots should share the styling.
addStyleTag({ path }) or { url } Page lifetime The stylesheet is maintained as a file or served from a reachable location.

4. Pick the screenshot scope and pixel scale

Playwright’s screenshot options let you choose what appears in the image and how CSS pixels map to output pixels.

  • fullPage: true captures the full scrollable page. Leave it off for the current viewport.
  • clip: { x, y, width, height } captures a rectangle in page coordinates. Use it for a known region; ensure the rectangle is within the rendered page.
  • scale: 'css' produces one output pixel per CSS pixel. scale: 'device' uses the device pixel ratio and produces higher-resolution output on high-DPI contexts.
  • path writes the image to a file. You can omit it and consume the screenshot bytes returned by the API.
  • type selects 'png', 'jpeg', or 'webp' where supported by the installed Playwright version; JPEG quality can be set with quality.
// Capture just the viewport at CSS-pixel scale.
await page.screenshot({ path: 'viewport.png', scale: 'css' });

// Capture the whole scrollable page.
await page.screenshot({ path: 'full-page.png', fullPage: true, scale: 'css' });

// Capture a rectangular area.
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 900, height: 600 },
});

Choose one scope deliberately. A full-page image can be very tall, consume more memory, and expose content that was below the fold. A clipped image is precise but can cut off content if layout or fonts change. CSS-scale output is convenient for pixel dimensions tied to layout; device-scale output is useful when the destination needs more pixels.

5. Stabilize the page before capture

Dynamic content can change between runs. Wait for a meaningful page condition rather than assuming that navigation completion means every image, font, widget, or client-side render is finished.

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.locator('main').waitFor({ state: 'visible', timeout: 10_000 });
await page.screenshot({
  path: 'stable.png',
  style: `
    *, *::before, *::after {
      animation: none !important;
      transition: none !important;
      caret-color: transparent !important;
    }
  `,
  fullPage: true,
});

For screenshots used in visual assertions, Playwright Test also supports a stylesheet path through screenshot assertion options. Styling can hide volatile regions such as embedded content and reduce unwanted visual differences. Other assertion controls include disabling animations, hiding the caret, masking selected locators, and setting comparison thresholds. These controls reduce common variation; they cannot make remote content, browser versions, operating systems, or fonts identical.

6. Build the agent loop

A practical agent workflow separates browser work from model decisions. Your application owns the browser session and supplies the agent with a page description and a small set of allowed actions. The agent returns a selector, CSS, or capture choice; your code validates and applies that result.

  1. Navigate to the target URL and wait for the page condition your use case requires.
  2. Read an ARIA snapshot or inspect known page structure.
  3. Ask the agent for a specific action, such as hiding a consent dialog or selecting a content region.
  4. Validate its CSS and selector choices against your task’s limits.
  5. Apply capture-only CSS or inject a persistent stylesheet.
  6. Capture viewport, full page, or a clip and store or return the resulting bytes.

Keep the browser session under your control. Treat page content and agent-generated CSS as untrusted input: constrain what actions can run, avoid evaluating arbitrary scripts from page text, and do not expose credentials in prompts or logs. If the agent needs to click a control before capture, use a known locator and check that the expected state changed before taking the image.

7. cURL, Python, and Node.js with Playwright

Playwright is a browser automation library, so cURL cannot perform the same rendered-browser workflow by itself. cURL can call a screenshot service that runs the browser capture for you. The following examples use ScreenshotNeo’s screenshot API with its documented endpoint and request shape.

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

These service examples show a basic one-call capture. For CSS, viewport, full-page capture, element selection, waiting, and other options, use the parameter names and examples in the ScreenshotNeo API documentation. ScreenshotNeo accepts the parameter names other screenshot APIs use as well, which can simplify a switch.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a PNG, JPEG, WebP, or PDF. Cookie banners are accepted like a visitor would accept consent, and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives Claude, Cursor, and other MCP clients the take_screenshot, get_page_info, and capture_pdf tools.

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

There are 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan. Read the API documentation, then sign up for 1,000 free screenshots a month with no card.

Troubleshooting

Symptom Likely cause Fix
CSS has no visible effect The selector does not match, the element is inside a frame or shadow root, or page styles override it. Inspect the rendered DOM and verify the selector. Use a more specific selector and !important where appropriate. Frame and shadow-root content may need separate handling.
The page is blank or incomplete Navigation returned before client rendering, the site failed, or a required element was not ready. Wait for a meaningful locator or page-specific condition. Check navigation errors and the target URL in the same browser environment.
Screenshot differs between runs Animation, timestamps, ads, asynchronous content, fonts, or remote state changed. Disable motion, hide or mask volatile regions, wait for fonts and required content, and keep browser, viewport, scale, and environment consistent.
Full-page capture is too large or slow The document is unusually long or triggers lazy loading. Use viewport or clip capture if sufficient, or capture sections separately. Avoid device scale unless the extra pixels are needed.
Local stylesheet fails to load The path is wrong or inaccessible from the process running Playwright. Resolve the file path relative to the script or pass an absolute path, and confirm the file is available in the runtime container.
Cookie dialog still appears The site’s dialog is rendered late, uses an unfamiliar selector, or resides in an embedded frame. Wait for it to appear, then use a page-specific locator or capture CSS. CSS hiding changes the image, but does not necessarily record consent or change site behavior.
Browser launch fails in deployment The Playwright browser binary is missing or the runtime lacks required browser dependencies. Install Chromium for the Playwright version in use and deploy the browser dependencies required by that environment.
The agent chose an unsafe or incorrect action Page text or model output was treated as trusted instructions. Limit actions to an allowlist, validate selectors and CSS, and keep secrets out of page context and prompts.

Performance, reliability, and cost

  • Performance: Reuse a browser process and create separate contexts for isolated jobs when running repeated captures. Full-page, high-DPI captures can use substantially more memory than viewport captures. Keep timeouts bounded and wait for the specific content your screenshot needs.
  • Reliability: Use a fixed browser version, viewport, device scale, and stylesheet for repeatable output. Remote pages still change, so visual sameness is not guaranteed. Record navigation and capture failures separately from valid images.
  • Cost: Self-hosted Playwright has no per-screenshot API charge, but requires compute, browser installation, and maintenance. ScreenshotNeo’s free plan includes 1,000 shots per month without a card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Every feature is on every plan.

FAQ

Can an AI agent generate the CSS automatically?

It can, if your application asks a model to inspect page information and return CSS. Playwright supplies browser APIs; model choice, prompts, validation, and permissions belong to your agent application.

Does capture-only CSS change the live webpage?

It is applied for the screenshot operation. Use addStyleTag() if later inspection or interactions need the stylesheet too.

Will CSS make screenshots identical across machines?

No. CSS can reduce variation from page elements, but browser rendering, fonts, operating systems, and remote page state can still differ.

Can I screenshot a single element instead of a rectangle?

With Playwright, locate the element and call its screenshot method, such as await page.locator('main').screenshot({ path: 'main.png' }). This follows the element’s rendered bounds; use a page clip when you need fixed coordinates.