Converting HTML to Images in JavaScript: The Complete Guide
html2canvas, Puppeteer, or a render API? A practical comparison of every approach for generating images from HTML server-side.
Converting HTML to an image is one of those tasks that sounds trivial and turns out to be surprisingly nuanced. The right approach depends heavily on where you're running the code — browser, Node.js server, or serverless function — and what fidelity you need in the output.
Why you'd want to do this
The most common use cases:
- Open Graph images: Generate a unique 1200×630 social card for every blog post, product, or listing page.
- Email headers: Email clients can't execute JavaScript, but they can display images — so a dynamically rendered PNG is the only way to personalize email visuals.
- Thumbnails: Preview images for videos, documents, or dashboard widgets.
- Certificates and badges: Personalized credentials that need to look designed, not generated.
- Social sharing: Quote cards, stat cards, code snippet images shared on Twitter/X.
Option 1: html2canvas (browser only, unreliable)
html2canvas re-implements a subset of CSS in JavaScript and "paints" the DOM to a <canvas> element, which you then export as a PNG. It works in a browser — but only for simple layouts. Any CSS that html2canvas hasn't implemented (many transforms, pseudo-elements, some flex behaviors, SVG backgrounds, webfonts from certain origins) silently produces wrong output or renders nothing. It also can't access cross-origin resources, which breaks most font and image references.
It's useful for quick internal tools where output fidelity isn't critical. It's not suitable for production user-facing images.
Option 2: Puppeteer screenshot (server-side, full fidelity)
Puppeteer can navigate to a URL or set HTML content and call page.screenshot(). This uses a real Chromium render pipeline, so the output is pixel-perfect — every CSS property, every font, every shadow renders exactly as a browser would display it.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(`
<div style="width:1200px;height:630px;background:#0a0a0a;
display:flex;align-items:center;justify-content:center;
color:white;font-size:64px;font-weight:bold;">
Hello world
</div>
`);
const image = await page.screenshot({ type: 'png' });
await browser.close();The downside is the same as for PDF generation: a 300 MB Chromium binary, Linux system dependencies, and difficulty running in serverless environments.
Option 3: Satori (Node.js, limited CSS)
Satori (by Vercel) converts a JSX tree to SVG using a custom CSS layout engine, which you can then convert to PNG via resvg-js. It's fast, lightweight, and genuinely serverless-friendly — no Chromium required.
The catch: Satori implements a subset of CSS. Only flexbox layout is supported (no grid, no absolute positioning). Text rendering differs from the browser. Custom fonts must be loaded explicitly as ArrayBuffers. Many Tailwind utilities don't work. For simple cards with a title and a background, it's excellent. For anything more complex, you'll hit its limits quickly.
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import fs from 'fs';
const svg = await satori(
{ type: 'div', props: { style: { background: 'black', color: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 64 }, children: 'Hello' } },
{ width: 1200, height: 630, fonts: [{ name: 'Inter', data: fs.readFileSync('./Inter.woff'), weight: 400, style: 'normal' }] }
);
const png = new Resvg(svg).render().asPng();Option 4: A render API (Tailwind, full CSS, serverless)
Render APIs run a real Chromium instance server-side and expose it over HTTP. You POST your HTML or Tailwind template and receive a PNG, JPEG, or WebP binary. Full CSS fidelity, no binary to ship, works from any runtime — Lambda, Edge Functions, a cron job, a plain Node.js server.
const res = await fetch('https://renderfy.io/api/v1/render', {
method: 'POST',
headers: {
'Authorization': 'Bearer rfy_live_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'tailwind',
content: `
<div class="flex h-full w-full flex-col justify-between bg-zinc-900 p-16 text-white">
<span class="text-sm font-medium text-emerald-400">renderfy.io</span>
<h1 class="text-6xl font-bold leading-none">Your post title here</h1>
</div>
`,
options: { format: 'png', dimensions: 'og-image' },
}),
});
const png = Buffer.from(await res.arrayBuffer());Decision guide
- Browser, quick tool, low fidelity OK: html2canvas
- Server, simple layout, no browser wanted: Satori + resvg
- Server, full CSS, you manage the infra: Puppeteer with a warm browser pool
- Serverless or edge, full CSS, no ops: Render API
For most production use cases generating OG images, email headers, or share cards, the render API model is the simplest path — you write a Tailwind template, call an endpoint, get a PNG. The infrastructure cost is someone else's problem.
Further reading
- How to Generate WebP Images in Node.js — why WebP is the better default format and how to generate it server-side
- HTML to Image API — full reference for converting HTML and Tailwind to PNG, JPEG, and WebP
- Open Graph Image API — generating dynamic 1200×630 OG images at build time or on demand