Create a Screenshot of a Page from HTML Code
Render HTML in a real browser, wait for assets and fonts, then capture the viewport, full page, element, or a clipped region.
Direct answer: render the HTML in a real browser, wait until the intended state is ready, then capture the viewport, full document, element, or selected rectangle. Playwright and Puppeteer both support this workflow. For raw markup, inject it with page.setContent(); for a live page, navigate to its URL.
What the workflow does
- Start a headless browser.
- Create a page with an explicit viewport.
- Load a URL or inject your HTML.
- Wait for fonts, images, data, and custom elements.
- Capture the required scope and format.
- Save the bytes or process them in memory.
- Close the browser.
A browser renderer is required because HTML alone does not resolve CSS, fonts, JavaScript, layout, or images. The examples below use JavaScript with Playwright, followed by Puppeteer equivalents.
Capture raw HTML with Playwright
Install Playwright and its browser binary:
npm install playwright
npx playwright install chromium
Create screenshot.mjs:
import { chromium } from 'playwright';
const html = `<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
* { box-sizing: border-box; }
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; background: #f6f7fb; color: #171923; }
main { max-width: 900px; margin: 48px auto; padding: 40px; background: white; border-radius: 16px; }
h1 { margin-top: 0; }
</style>
</head>
<body>
<main>
<h1>Rendered from HTML</h1>
<p>This image was produced by a browser screenshot.</p>
</main>
</body>
</html>`;
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 1
});
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', type: 'png' });
await browser.close();
Playwright’s documented screenshot API also returns a buffer when you omit path, which is useful for uploads, hashing, or pixel comparisons. See the Playwright screenshots guide and Page API.
Choose the capture area
Viewport only
await page.screenshot({ path: 'viewport.png' });
This captures what fits inside the current viewport.
Full scrollable document
await page.screenshot({ path: 'full-page.png', fullPage: true });
fullPage: true includes content below the fold. A very long document can produce a very tall image; capture a component or clip when a focused result is more useful.
One element
await page.locator('.invoice').screenshot({ path: 'invoice.png' });
The locator screenshot sizes the output to the selected element. Wait for the locator to be visible and stable if its contents are rendered asynchronously.
A rectangle
await page.screenshot({
path: 'region.png',
clip: { x: 100, y: 80, width: 900, height: 500 }
});
Coordinates are CSS pixels relative to the page viewport. Keep the clip inside the viewport unless your browser version supports the required scrolling behavior.
Output formats, scale, and visual state
| Need | Settings |
|---|---|
| Lossless output | PNG, the default choice for text, diagrams, and pixel comparison. |
| Small photographic output | JPEG with a quality value. |
| Modern compressed output | WebP with a quality value where supported. |
| High-density output | Set deviceScaleFactor to 2 or another explicit value. |
| One image pixel per CSS pixel | Use deviceScaleFactor: 1. |
| Transparent background | Use omitBackground: true with a format that preserves alpha. |
| Deterministic animation | Disable animations in CSS or use Playwright’s animation controls where available. |
await page.screenshot({
path: 'card.webp',
type: 'webp',
quality: 82,
animations: 'disabled'
});
Use the same browser version, viewport, device scale, fonts, timezone, locale, and data for reproducible images. Avoid timestamps, random IDs, rotating ads, and live counters in visual tests.
Wait for the rendered state
setContent(..., { waitUntil: 'load' }) only tells you that the load event fired. Applications may still be fetching data or decoding images. Add waits that describe the state you actually need:
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.waitForSelector('.chart[data-ready="true"]');
await page.waitForTimeout(250);
For a URL, wait during navigation and then wait for the page-specific readiness condition:
await page.goto('https://example.com/report', { waitUntil: 'networkidle' });
await page.waitForSelector('#report-ready');
await page.evaluate(() => document.fonts.ready);
Use a bounded timeout. Waiting forever hides failures and consumes browser resources.
Raw HTML details that commonly affect screenshots
- Fonts: await
document.fonts.ready; package or self-host fonts for CI when external font services are unreliable. - Images: use valid absolute URLs or data URLs, and wait for decoding when images are injected after load.
- Lazy content: scroll the page or trigger the application’s loading condition before a full-page capture.
- Web components: wait for the component’s ready attribute or selector.
- Canvas and charts: wait for the drawing code to finish; a DOM selector alone may not mean pixels are ready.
- Cross-origin frames: frame content may have separate loading and authentication requirements.
- Fixed headers: a full-page capture can repeat or overlap fixed-position elements; test the result and use a clip or CSS adjustment if needed.
- Responsive layouts: set width and height explicitly; do not rely on the host machine’s default viewport.
Puppeteer alternative
Install Puppeteer:
npm install puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.setContent(`<!doctype html><html><body><h1>Hello</h1><p>Rendered from HTML.</p></body></html>`, {
waitUntil: 'load'
});
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
Puppeteer documents setContent, Page.screenshot(), element screenshots, binary output, and base64 output in its screenshots guide and Page.screenshot API.
Capture an element or process bytes in memory
const image = await page.locator('#receipt').screenshot();
await Bun.write('receipt.png', image);
In Node.js, replace the final write with writeFile, an object-store upload, or an image-processing pipeline. Keeping the buffer in memory avoids temporary files but increases peak memory for large full-page images.
Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API and MCP server. Give it a deployed HTML page URL and receive PNG, JPEG, WebP, or PDF output. The API removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.
See the ScreenshotNeo API documentation for all options, including full-page and element capture, custom CSS and JavaScript, waits, blocking rules, headers, cookies, user agents, authentication, timezone, geolocation, caching, signed links, asynchronous jobs, bulk capture, and PDF settings.
cURL
curl -G 'https://api.screenshotneo.com/v1/shot' \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
'https://api.screenshotneo.com/v1/shot',
params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.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://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await Bun.write('shot.webp', Buffer.from(await res.arrayBuffer()));
An MCP server is included for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no charge.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or partially rendered image | Capture ran before data, fonts, or images were ready. | Wait for a readiness selector, document.fonts.ready, and image decoding. |
| Missing lazy images | They load only after scrolling or intersection. | Scroll through the page or use an application-specific preload condition before fullPage. |
| Wrong dimensions | Default viewport or device scale varies by environment. | Set viewport width, height, and device scale explicitly. |
| Timeout during navigation | A request never settles, often because of analytics or streaming connections. | Use a practical navigation timeout and wait for a page-specific selector instead of global network idle. |
| Fonts differ in CI | The font is unavailable or loads from an external service. | Install or bundle the font and wait for document.fonts.ready. |
| Element screenshot fails | The selector matches nothing, is hidden, or is detached. | Check the selector, wait for visibility, and capture after the component mounts. |
| Full-page output is enormous | The document is genuinely tall or contains an expanding element. | Capture a target element or clip, remove unintended expansion, and inspect page height. |
| Authentication or blocked resources | The page requires headers, cookies, or a permitted network route. | Configure request headers and cookies in your browser context, or use the corresponding ScreenshotNeo options. |
| ScreenshotNeo response is not an image | The target returned a bot check, blank page, timeout, or other verdict. | Read X-Page-Verdict, inspect the response status and body, and fix the target or capture settings. |
Performance, reliability, and cost
- Reuse a browser process for multiple captures, but create isolated pages or contexts so cookies and state do not leak between jobs.
- Limit concurrency to the memory available to your worker. Full-page and high device-scale screenshots use more memory than viewport captures.
- Prefer PNG for visual diffs and text-heavy images; use JPEG or WebP when smaller files matter.
- Cache stable pages and assets where appropriate. For ScreenshotNeo, choose a cache TTL when repeated captures do not need fresh rendering.
- Record the viewport, browser version, HTML or URL revision, font versions, and capture options with each artifact.
- Retry transient browser or network failures with a bounded backoff. Do not blindly retry deterministic selector and markup errors.
- Close pages and browsers in a
finallyblock in production workers. - With ScreenshotNeo, only clean shots are billed; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Every plan includes every feature: Free 1,000 per month, Starter $5/3,000, Growth $15/15,000, Pro $39/60,000, Scale $99/250,000, and Business $249/1,000,000. Yearly billing gives two months free.
FAQ
Can I screenshot HTML without hosting it?
Yes. Inject the markup with Playwright or Puppeteer setContent and capture the page. A hosted screenshot API generally expects a URL, so deploy the page or use an HTML-to-image feature when available.
Should I use full-page or element capture?
Use full-page for document snapshots and element capture for cards, invoices, charts, and components. Element capture avoids excessively tall images.
Why does my screenshot change between runs?
Common causes are fonts, animations, responsive viewport differences, changing data, timestamps, ads, and asynchronous requests. Fix each by controlling inputs and waiting for a defined ready state.
Which format should I choose?
Use PNG for lossless text and comparison workflows. Use JPEG or WebP for smaller files when a little quality loss is acceptable.


