HTML to image in Node.js

Three ways to turn an HTML string into a PNG from Node.js, all run on the same 600×315 social card. Tested with node-html-to-image 6.2, Puppeteer 25 and Node 22.

Quick answer

OptionPick it when
node-html-to-image
A Puppeteer wrapper
You want Handlebars templates and batches in a few lines, and don't mind Chromium in your node_modules.
Puppeteer
Chromium, full control
You need element crops, waits for fonts or charts, or one browser reused for thousands of images.
An API
Chromium on someone else's server
Vercel, Lambda or a slim Docker image where a 150 MB browser doesn't fit.

Satori (what @vercel/og uses) is a different approach: it turns JSX into SVG without a browser, so it's very fast on the edge. The catch is that it supports a subset of CSS (flexbox, no grid), and you load font files yourself, so it can't render arbitrary HTML or a live page. Use it if you write the card as JSX from scratch. html2canvas runs in the visitor's browser, not in Node; see the html2canvas alternative guide.

The test card

Save this as card.html: a fixed-size box with a gradient and flexbox layout, like most generated images.

<!doctype html>
<html><head><meta charset="utf-8"><style>
  body { margin: 0; font-family: sans-serif; }
  .card { width: 600px; height: 315px; box-sizing: border-box; padding: 40px;
          display: flex; flex-direction: column; justify-content: space-between;
          background: linear-gradient(135deg, #4f46e5, #0ea5e9); color: #fff; }
  h1 { margin: 0; font-size: 40px; }
</style></head><body>
  <div class="card"><h1>Release 2.4 is out</h1><p>Faster builds, smaller bundles.</p></div>
</body></html>

The snippets below use top-level await, so save them as .mjs files (or set "type": "module" in package.json).

1. node-html-to-image

npm install node-html-to-image

It installs Puppeteer (and its Chromium) as a dependency and screenshots the page for you.

import nodeHtmlToImage from "node-html-to-image";
import { readFile } from "node:fs/promises";

const html = await readFile("card.html", "utf8");
await nodeHtmlToImage({
  output: "./card.png",
  html,
  selector: ".card",
  puppeteerArgs: { defaultViewport: { width: 1200, height: 800, deviceScaleFactor: 2 } },
});

Result: a 1200×630 PNG (600×315 at 2×). Watch the defaults: without selector it captures body in an 800px-wide window, so our 600px card came out as an 800×315 image with a white strip on the right. Set selector to your card's element and it crops exactly. Other things to know:

2. Puppeteer

npm install puppeteer
import puppeteer from "puppeteer";
import { readFile } from "node:fs/promises";

const html = await readFile("card.html", "utf8");
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 2 });
await page.setContent(html, { waitUntil: "networkidle0" });
await page.evaluate(() => document.fonts.ready);
const card = await page.$(".card");
await card.screenshot({ path: "card.png" });
await browser.close();

Result: the same 1200×630 PNG, cropped to the card. This is what node-html-to-image does underneath, so you get the same pixels with full control. Variations:

3. An API (no browser to install)

Shotmint renders in the same Chromium, but your code only makes an HTTP request, so it works on Vercel or Lambda as is. selector crops to an element like card.screenshot():

import { readFile, writeFile } from "node:fs/promises";

const html = await readFile("card.html", "utf8");
const res = await fetch("https://shotmint.creasework.com/v1/screenshot", {
  method: "POST",
  headers: { "X-API-Key": process.env.SHOTMINT_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ html, selector: ".card", device_scale_factor: 2 }),
});
if (!res.ok) throw new Error(await res.text());
await writeFile("card.png", Buffer.from(await res.arrayBuffer()));

Result: the same 1200×630 PNG. Other options: format (png, jpeg, webp), quality, width/height for the viewport, full_page, omit_background, delay in milliseconds to wait for scripts, and "url": "https://..." to capture a live page instead of HTML. Images and fonts must be reachable from the internet or inlined as data URIs. See the Node.js API guide for retries and error handling.

Side by side

FeatureSupport
Crop to an elementAll three (selector, element.screenshot())
node-html-to-image defaults to body at 800px wide
Templates and batchesnode-html-to-image: built in (Handlebars)
Puppeteer and API: loop and fill the HTML yourself
Retina (2×)All three (deviceScaleFactor, device_scale_factor)
Install sizeAPI: none (built-in fetch)
node-html-to-image and Puppeteer: plus a Chromium download

Which one?

For batches of templated images on a server you control, node-html-to-image is the least code; just set selector. If you need waits, element crops or browser reuse, use Puppeteer directly. On serverless or in a slim container, the API skips the browser entirely. No code at all? Paste your HTML into the free HTML to image converter. Social cards from a template: dynamic og:images. Python instead: HTML to image in Python. PDFs: HTML to PDF in Node.js.

Try it with 100 free renders

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).

Get a free API key Full API reference