How to Render HTML as an Image
Render HTML to PNG, JPEG, or WebP with a real browser. Compare Puppeteer, Playwright, and ScreenshotNeo, with code, options, and troubleshooting.
To render HTML as an image, load it in a browser engine and capture the rendered page. The browser resolves CSS, fonts, images, JavaScript, and layout before its screenshot API writes PNG, JPEG, or WebP bytes. For a page you control, use Puppeteer or Playwright. For an HTTP workflow, use a hosted screenshot API.
A reliable pipeline is:
- Create a page from a URL or an HTML string.
- Wait for the content and assets your image needs.
- Choose viewport, full-page, or clipped capture.
- Set format, quality, scale, and background behavior.
- Save the returned bytes and close the browser.
1. Render HTML with Puppeteer
Puppeteer controls Chromium and exposes Page.screenshot(). This example renders an HTML document, waits for fonts and images, and saves a full-page PNG.
import puppeteer from 'puppeteer';
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0; font: 20px system-ui; background: #f5f7fb; }
.card { width: 720px; margin: 48px auto; padding: 32px;
background: white; border-radius: 16px; }
</style>
</head>
<body><main class="card"><h1>Rendered HTML</h1><p>Captured by Chromium.</p></main></body>
</html>`;
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.evaluate(async () => { await document.fonts.ready; });
await page.waitForFunction(() =>
[...document.images].every(image => image.complete));
await page.screenshot({ path: 'rendered.png', fullPage: true });
} finally {
await browser.close();
}
Install it with npm install puppeteer. If your HTML references local files, use absolute file:// URLs carefully or serve the directory over a local HTTP server. Remote images and fonts must be reachable from the browser process.
Render a URL instead of an HTML string
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'page.png', fullPage: true });
networkidle0 is useful for pages that finish loading, but it is not proof that every animation, lazy image, or client-side request is complete. Add an explicit selector wait or application-ready signal when necessary.
2. Render HTML with Playwright
Playwright provides equivalent browser capture controls through its Page API. The following Python example renders an HTML string and writes a PNG.
from pathlib import Path
from playwright.sync_api import sync_playwright
html = """<!doctype html>
<html><head><style>
body { margin: 0; font: 20px system-ui; background: #f5f7fb; }
.card { width: 720px; margin: 48px auto; padding: 32px; background: white; }
</style></head>
<body><main class='card'><h1>Rendered HTML</h1></main></body></html>"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 900}, device_scale_factor=1)
page.set_content(html, wait_until="networkidle")
page.evaluate("document.fonts.ready")
page.screenshot(path="rendered.png", full_page=True, type="png")
browser.close()
Install with pip install playwright followed by playwright install chromium. Playwright also supports JavaScript, Java, and .NET bindings; use the binding that matches your application.
3. Choose the capture area
| Goal | Setting | Use it when |
|---|---|---|
| Viewport screenshot | Default screenshot | You need exactly what is visible at a fixed width and height. |
| Whole document | Puppeteer fullPage: true or Playwright full_page=True |
You need a long article, invoice, or landing page in one image. |
| One region | Puppeteer clip or Playwright locator.screenshot() |
You need a card, chart, or element instead of the page. |
// Puppeteer: clip a CSS-pixel rectangle
await page.screenshot({
path: 'region.png',
type: 'png',
clip: { x: 100, y: 120, width: 800, height: 500 }
});
// Playwright: capture one element
await page.locator('.card').screenshot({ path: 'card.png' });
Coordinates are CSS pixels. A device scale factor greater than one increases output pixels and file size without changing the CSS layout.
4. Image format and rendering options
- PNG: lossless and suitable for text, diagrams, and transparency.
- JPEG: smaller for photographic content; choose a quality value where the library supports it.
- WebP: compact output when your consumers support it.
- Scale: use a device scale factor or Playwright’s
scaleoption when you need more pixels per CSS pixel. - Transparent background: Puppeteer’s
omitBackground: truehides the default white page background where transparency is supported. - Viewport: set width, height, and device scale factor before navigation so responsive CSS selects the intended breakpoint.
- Media mode: set print or screen media deliberately when your stylesheet has separate rules.
- Animations: disable transitions and animations for deterministic output, or wait for a known animation state.
await page.emulateMediaType('screen');
await page.addStyleTag({ content: `
*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
` });
await page.screenshot({ path: 'stable.webp', type: 'webp', quality: 85 });
For PDFs, use the browser’s PDF API instead of converting a screenshot. A PDF preserves document structure differently from a raster image.
5. Make assets and dynamic content deterministic
Wait for the right condition
Use a combination of navigation, a required selector, font readiness, and image completion. A fixed sleep alone is fragile because network and rendering times vary.
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-render-ready]', { timeout: 30000 });
await page.evaluate(async () => {
await document.fonts.ready;
await Promise.all([...document.images].map(image =>
image.complete ? Promise.resolve() : new Promise(resolve => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
})
));
});
Handle lazy loading
Scroll through a long page before capture so intersection-observer images are requested. Then wait for the final image set. This can increase time and memory use on very long documents.
Control external resources
Remote fonts, analytics, ads, and third-party widgets can change layout or delay readiness. Block resources you do not need, self-host critical fonts, and provide fallbacks for failed images. If the page requires authentication, establish cookies or headers before navigation.
6. Repeatability, reliability, and security
Pixel output can differ between operating systems, browser versions, installed fonts, hardware, power settings, and headless modes. Playwright’s visual comparison guidance recommends using the same environment as the baseline. Pin your browser and library versions for visual tests, run captures in a consistent container, and keep fonts available in that environment.
- Set an explicit viewport, timezone, locale, color scheme, and reduced-motion preference.
- Use stable test data and freeze timestamps when the page displays the current time.
- Wait for a semantic ready marker rather than an arbitrary delay.
- Give navigation and screenshot operations timeouts, then close the browser in a
finallyblock. - Treat untrusted HTML as untrusted code. Isolate browser processes, restrict network access where appropriate, and never expose privileged credentials to page JavaScript.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank or partly blank image | Capture occurred before client rendering completed. | Wait for a required selector, fonts, and images; inspect console and network errors. |
| Missing images | Lazy loading, blocked requests, bad relative URLs, or CORS/authentication. | Use an absolute base URL, scroll to load lazy assets, and configure cookies or headers. |
| Wrong responsive layout | Default viewport or device scale differs from the target. | Set viewport width and height before goto; set device scale separately. |
| Fonts look different | Font files are unavailable or the rendering environment differs. | Bundle or preload fonts and pin the browser/OS environment. |
| Full-page capture cuts content | Content expands after the initial measurement. | Wait for the final content marker, scroll lazy sections, then capture again. |
| Timeouts | Slow dependency, never-ending request, bot check, or selector that never appears. | Set a realistic timeout, block unnecessary resources, and fail with a diagnostic page URL and selector. |
| Large files or slow jobs | Very tall page, high device scale, or lossless format. | Capture an element, reduce scale, use JPEG/WebP where acceptable, or split long content. |
8. Performance and cost planning
Browser startup is usually more expensive than taking another page screenshot. Reuse a browser process for batches, create isolated pages or contexts per job, and close each page after capture. Limit concurrency to the CPU and memory available; too many simultaneous Chromium pages cause contention and timeouts.
Cache unchanged HTML and assets, block tracking resources, and avoid full-page captures when a component screenshot meets the requirement. Local automation has infrastructure costs for browser binaries, memory, patches, and operations. A hosted API trades that setup for request charges and service-specific limits, so measure your own latency, output size, and volume.
9. Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted before capture, then more than 60 known consent platforms, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for the request options. A minimal call is:
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}`);
It also supports full-page and element capture, dark mode, device presets and custom viewports, retina scale, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching with your 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. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Can I render an HTML string without hosting it?
Yes. Use Puppeteer’s page.setContent() or Playwright’s page.set_content(), then wait for referenced assets before taking the screenshot.
Should I use a screenshot or a PDF?
Use an image for pixels in a preview, report, or visual test. Use a PDF when pagination, selectable text, and document output matter.
Why does the same HTML produce different pixels?
Browser version, operating system, fonts, hardware, settings, and headless mode can affect rendering. Keep the capture and comparison environments consistent.
How do I capture only a component?
Use Puppeteer’s clip rectangle or Playwright’s locator screenshot. Waiting for the component’s own ready state avoids capturing a partially rendered region.


