ScreenshotNeo

BlogHow-to

How to Convert HTML to a Screenshot with an API

Render HTML in a browser, capture it with Playwright or a hosted API, and choose the right format, timing, viewport and reliability settings.

By the ScreenshotNeo team1 October 20268 min read

Direct answer: HTML must be rendered by a browser engine before it can become a screenshot. You can run that browser yourself with Playwright, or send a URL or HTML document to a hosted screenshot API that performs the rendering and returns image bytes (or, depending on the provider, a URL).

This guide shows both approaches, including complete code, timing and viewport decisions, output formats, failure handling, and production considerations.

1. What “convert HTML to a screenshot” actually means

A screenshot captures rendered pixels, not the source markup. The browser parses HTML, applies CSS, runs JavaScript, loads fonts and images, and lays out the page. Only after that work can an image be captured. Cloudflare describes its screenshot endpoint in these terms: it renders the webpage by processing HTML and JavaScript before capture (Cloudflare Browser Run documentation).

There are two practical inputs:

  • A URL: the browser navigates to an existing page.
  • Raw HTML: your application supplies a document, which the browser loads before capture.

Decide these output details before implementing:

Decision Typical choices Why it matters
Page area Viewport, full page, clipped region, element Full-page images include content below the fold; viewport images are smaller and faster.
Format PNG, JPEG, WebP PNG preserves sharp text and transparency; JPEG is smaller for photos; WebP often balances both.
Scale CSS pixels or a higher device scale Higher scale improves detail but increases bytes and rendering cost.
Readiness Load event, selector, network idle, explicit delay Capturing too early produces missing fonts, images or JavaScript content.
Response Binary bytes or a hosted URL Your client must either save bytes directly or fetch the returned URL.

2. Self-hosted conversion with Playwright

Playwright’s documented flow is: launch a browser, create a page, navigate (or load content), call the screenshot API, then close the browser. Its screenshot API supports full-page capture, clipping, image type, scale and buffer output (Page API; Screenshots guide).

2.1 Install Playwright

mkdir html-shot
cd html-shot
npm init -y
npm install playwright
npx playwright install chromium

2.2 Capture a URL

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });

    await page.goto('https://example.com', {
      waitUntil: 'networkidle',
      timeout: 30_000
    });

    await page.screenshot({
      path: 'page.png',
      fullPage: true,
      type: 'png'
    });
  } finally {
    await browser.close();
  }
})();

networkidle is useful for pages that finish loading their assets, but it is not a universal definition of “ready.” Analytics, chat clients and other long-lived connections can prevent it. For those pages, wait for a meaningful selector or use a bounded delay after the selector appears.

2.3 Capture application-generated HTML

Use page.setContent() when the HTML is already in your application. Include CSS and any assets with reachable URLs. Inline assets or serve them from an address the browser can access.

const { chromium } = require('playwright');

const html = `<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      body { margin: 0; font-family: sans-serif; background: #f6f7fb; }
      .card { width: 720px; margin: 40px auto; padding: 32px;
              background: white; border-radius: 16px; }
    </style>
  </head>
  <body>
    <main class="card"><h1>Invoice preview</h1><p>Ready to capture.</p></main>
  </body>
</html>`;

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({ viewport: { width: 900, height: 700 } });
    await page.setContent(html, { waitUntil: 'load', timeout: 30_000 });
    await page.locator('main.card').waitFor({ state: 'visible', timeout: 10_000 });
    await page.screenshot({ path: 'html.png', type: 'png' });
  } finally {
    await browser.close();
  }
})();

2.4 Capture an element or return bytes

const pngBytes = await page.locator('.card').screenshot({ type: 'png' });
// Store pngBytes in object storage, return it from an HTTP handler, or attach it to a response.

const clipped = await page.screenshot({
  type: 'webp',
  quality: 85,
  clip: { x: 0, y: 0, width: 800, height: 500 }
});

Element screenshots avoid capturing unrelated page content. A clip uses page coordinates, so confirm the viewport and scroll position first.

2.5 Readiness checklist for dynamic HTML

  1. Wait for the main content selector.
  2. Wait for web fonts if typography affects layout.
  3. Ensure images have loaded; lazy images may require scrolling or an explicit load step.
  4. Disable animations when deterministic pixels matter, for example with injected CSS.
  5. Use a timeout that matches the page rather than an unbounded wait.
await page.addStyleTag({
  content: `*, *::before, *::after {
    animation-duration: 0s !important;
    transition: none !important;
  }`
});
await page.locator('#report').waitFor({ state: 'visible', timeout: 15_000 });

3. Hosted screenshot APIs

A hosted API runs the browser outside your application. This removes browser installation and process management, but every provider has its own contract. ScreenshotAPI documents URL or raw HTML input sent through POST and binary image data in the response body (ScreenshotAPI documentation). Cloudflare documents a /screenshot endpoint that renders HTML and JavaScript before capture (Cloudflare documentation).

Check the current provider documentation for accepted input, authentication, limits, supported formats, timeout behavior, and whether the result is bytes or a URL. Do not assume that one provider’s parameters work on another.

3.1 Generic hosted request pattern

curl -X POST "https://provider.example/screenshot" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "html": "<html><body><h1>Hello</h1></body></html>",
    "format": "png",
    "full_page": true,
    "width": 1200,
    "height": 800
  }' \
  --output shot.png

Replace the endpoint and field names with the provider’s documented values.

4. Or skip the browser setup

ScreenshotNeo provides a hosted website screenshot API. One GET request returns PNG, JPEG, WebP or PDF. It can capture a URL, render full pages with lazy images, select an element, set a viewport or device preset, use retina scale, wait for a selector, delay or network idle, and apply custom CSS or JavaScript. See the ScreenshotNeo API documentation for the complete option set.

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}`);

Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

For production workflows, its options include custom headers, cookies, user agent, Authorization, timezone, geolocation, transparent backgrounds, image resizing, request blocking, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification.

Create a free ScreenshotNeo account: 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan.

5. Choosing between Playwright and a hosted API

Question Playwright you operate Hosted API
Browser operations You install browsers, patch them and manage concurrency. The provider operates the browser service.
Input Any HTML or URL your process can reach. Only the documented URL or HTML contract.
Control Direct access to browser contexts, scripts and network behavior. Provider-specific options and limits.
Scaling You design queues, workers and memory limits. Use provider quotas, rate limits and job APIs.
Cost Account for compute, browser memory, operations and engineering time. Account for per-shot pricing, quotas and overage rules.
Data handling You control where HTML and screenshots run. Review retention and processing terms.

The sources do not establish a universal speed or cost winner. Measure your own page sizes, JavaScript behavior, concurrency and required image dimensions.

6. Reliability and performance practices

  • Reuse browser processes: for self-hosting, keep a controlled pool of workers instead of launching an unbounded browser per request.
  • Bound every wait: set navigation, selector and overall job timeouts.
  • Limit concurrency: screenshots consume CPU and memory, especially at high device scale or full-page height.
  • Use the smallest capture: element or clipped screenshots reduce transfer size and processing.
  • Cache deliberately: cache only when the page can be stale; include the relevant URL, options and content version in your cache key.
  • Retry selectively: retry transient network failures with backoff, but do not blindly retry authentication errors, invalid HTML or bot challenges.
  • Record metadata: keep the requested URL, viewport, format, elapsed time, response status and failure reason.
  • Protect secrets: keep API keys server-side and avoid placing them in public image URLs unless using a provider’s signed-link feature.

7. Troubleshooting

Symptom Likely cause Fix
Blank or partially empty image Capture happened before content rendered. Wait for a meaningful selector, fonts and images; increase a bounded timeout.
Lazy images missing They load only after scrolling or intersection. Scroll through the page or use a capture service that loads lazy images.
Fonts look wrong Font files were inaccessible or not finished loading. Check font URLs and wait for document.fonts.ready.
Full-page screenshot is cut off Page height changed during capture or the provider defaults to viewport mode. Use the documented full-page option and wait for late content.
Navigation timeout Slow origin, blocked resource or never-ending connection. Inspect the URL, block unnecessary resources, use a larger bounded timeout, or wait for a selector instead of network idle.
401/403 response Missing or invalid API key, Authorization header or target access. Verify credentials and send required headers; never expose keys in client code.
CAPTCHA or bot-check image The target challenged the browser. Do not treat it as page content; use an authorized access path and inspect the provider’s verdict metadata.
Huge files or slow jobs Very large dimensions, full-page height or high device scale. Capture an element, reduce dimensions or scale, and choose WebP/JPEG when appropriate.
Animations differ between runs Capture timing is nondeterministic. Disable transitions, wait for a stable state and use fixed viewport and timezone settings.

8. Short FAQ

Can an HTML-to-image library skip a browser?

Only for limited CSS and layout support. Browser rendering is the reliable path when the page depends on modern CSS, web fonts or JavaScript.

Should I return a file path or bytes?

Use bytes when your service will upload or stream the result. Use a path for local batch jobs. Hosted APIs may return bytes or a URL, so follow their contract.

When is full-page capture the wrong choice?

Use viewport, clipped or element capture for thumbnails, cards, dashboards and other bounded regions. Full-page images can become very tall and expensive to process.

How do I make repeated captures deterministic?

Fix the viewport, scale, timezone and authentication state; wait for a known selector; disable animations; and control external resources where your workflow permits.

Where can I find ScreenshotNeo’s parameter names?

Use the ScreenshotNeo documentation. Its parameter names also support the names used by other screenshot APIs, which helps when switching.