node-html-to-image: rendering HTML to PNG or JPEG with headless Chromium
A Node.js module that generates images from HTML
At a glance
- What is it?
- node-html-to-image is a Node library that turns an HTML string into a PNG or JPEG by driving headless Chromium through Puppeteer, with Handlebars templating, an option table covering viewport, quality and encoding, and a cluster mode for producing many images at once. It suits anyone who needs server-side images from markup.
- Who is it for?
- node-html-to-image is a good fit for a backend that needs to render a receipt, a share card or a social image from markup, and that wants a small API over Puppeteer rather than a browser automation framework.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 43 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A thin wrapper over Puppeteer, which is the point
The library does one thing and the mechanism is explicit. This module exposes a function that generates images, png and jpeg, from HTML, and it uses Puppeteer in headless mode to achieve it, additionally embedding Handlebars to provide a way to add logic in your HTML. The value of the wrapper is that the surrounding conventions are already decided for you. The minimal call names an output path and an HTML string and returns a promise:
const nodeHtmlToImage = require('node-html-to-image')
nodeHtmlToImage({
output: './image.png',
html: '<html><body>Hello world!</body></html>'
})
.then(() => console.log('The image was created successfully!'))The alternative, driving Puppeteer yourself, means deciding the viewport, the device scale factor, when to take the shot, whether to wait for fonts, and how to handle a page that has resources that never load. The wrapper has made those decisions and exposes them as options, and the option table in the readme is unusually complete, which is where you should look before writing a first call. The library is written in TypeScript so the types are available out of the box, with the import shown in the readme as a default import from the package name, and the published package carries its own type declarations, so a TypeScript project gets the option names checked.
The options table decides when the screenshot is taken
A few options in the table are worth reading closely because they are where screenshot output goes wrong. The waitUntil option defines when to consider the markup succeeded, and it accepts load and domcontentloaded, with the note that puppeteer no longer supports networkidle0 or networkidle2 for setContent, and the default is load. That last point is a trap: load fires when the initial resources are done, which is not the same as fonts and images having rendered, so text can be captured in a fallback font. The two hook options are the escape valve. The beforeRendering option is an async function that executes before the HTML is set on the page, before setContent, and it gives access to the Puppeteer page so you can intercept requests or set the viewport. The beforeScreenshot option is an async function that executes just before the screenshot is taken, and it gives access to the page element. Between them they cover the two moments where you need to intervene, before the content goes in and after it is there. The other options that matter day to day are the selector, which targets a specific element to screenshot and defaults to body, the type, which is jpeg or png with png as the default, the quality number, which applies only to jpg and defaults to 80, the transparent flag for a transparent background with png, and the encoding, which is binary by default or base64.
Handlebars templating, and the problem of images in the markup
The content option is what makes the library more than a screenshotter. If it is provided, the html property is considered a Handlebars template and the content value is used to fill it, and the content can be an object or an array, so a list of items can be rendered into rows. The readme has a separate section on Handlebars helpers, which implies the template can call helpers, and Handlebars is a dependency in the package metadata at a pinned 4.7.9. This is the mechanism that turns a static string into a parameterised render, which is what you want for a receipt or a notification card. The section on dealing with images covers the other hard part. A screenshot of markup that references an image by URL requires that image to load before the shot is taken, and if the image is behind authentication or on a host your browser session cannot reach, it will be missing from the output. The options for that are the interception hook beforeRendering and the waitUntil timing, and the practical advice is to inline images as data URLs when you control the markup, since a data URL cannot fail to load.
Getting a buffer instead of a file, and generating many images
Two features change how the library fits into a service. The first is the buffer option, described in the readme as using the buffer instead of saving to disk, which is what you want when the image is going straight into object storage or into an HTTP response, and the encoding option interacts with it, since the result can be a binary buffer or a base64 string. Writing to a temporary file and reading it back is a common pattern in scripts that this avoids. The second is generating multiple images, and the package metadata shows why that is worth a separate feature. The dependencies include puppeteer-cluster alongside puppeteer itself, pinned at 25.3.0 and 0.25.0 respectively, and puppeteer-cluster is included, which is how the same API can be driven by a cluster of browsers rather than a single one, which is the difference between rendering a hundred images taking a minute and taking a minute times a hundred. The readme also documents using different puppeteer libraries, with the puppeteer option taking an object so you can substitute puppeteer-core or puppeteer-extra, and puppeteerArgs for custom launch configuration, which is what lets you point it at an existing browser instead of the bundled one.
The Chromium download is the deployment cost you inherit
The install section has one sentence that deserves attention from anyone planning a deployment. When you install Puppeteer, it downloads a recent version of Chromium, about 170 megabytes on Mac, about 282 megabytes on Linux and about 280 megabytes on Windows, and the stated reason is that the version is guaranteed to work with the API. So the footprint of this library is not a few hundred kilobytes of JavaScript, it is a browser. That is fine for a development machine and for a container built with the Puppeteer install, and it is a problem for a small lambda or a container with a tight image size budget. The escape hatch is the puppeteer option, which lets you pass a different puppeteer library, so you can use puppeteer-core against a Chromium you already have in the image, or puppeteer-extra, or point at a browser you manage yourself. The puppeteerArgs option then carries the launch configuration. The engines field in the package metadata requires Node 22.12.0 or newer, and the release history shows three versions on the same day, with v6.0.0, v6.1.0 and v6.2.0 all on 2026-07-13, which is what a major version plus its immediate fixes looks like.
A small, focused codebase with a migration guide
The repository is compact and tells you the project is a maintained wrapper rather than a framework. The root has the src directory, a scripts directory, an eslint configuration, a tsconfig and a separate tsconfig for type checking, a vitest configuration, a CHANGELOG, a CODE_OF_CONDUCT, a LICENSE, a MIGRATION.md and the readme. The build is a plain tsc compile into dist, the test script runs vitest, and the release script is a node script in the scripts directory rather than a third-party release tool. The dev dependencies are revealing about the testing approach. There is tesseract.js, an OCR library, which is a strange thing to see in a screenshot library unless the test suite verifies the rendered output by reading text back out of the image, and that is a much stronger test than asserting that a file exists. There is puppeteer-core alongside the puppeteer dependency, so the tests can drive a browser without downloading one, and vite is present for the test fixtures. The MIGRATION.md file is worth reading if you are coming from an earlier major version, since a 6.0 release with three versions shipped on the same day is a sign of breaking changes. The licence is Apache-2.0, the last push was on 2026-09-11, and the package version is 6.2.0.
Editorial conclusion
node-html-to-image is a good fit for a backend that needs to render a receipt, a share card or a social image from markup, and that wants a small API over Puppeteer rather than a browser automation framework. It is the wrong tool for a workload that renders thousands of images, since each call launches a browser and a Chromium download of roughly 170 to 280 megabytes is part of the install, and it is the wrong tool if you need vector output, since the format is limited to jpeg and png. It is also the wrong tool if your environment cannot run a headless browser at all, for example a small serverless function with a tight memory limit. Before adopting it, read the options table rather than relying on the defaults, particularly the waitUntil option, because puppeteer no longer supports the networkidle values and the default of load will capture before fonts and images have settled, and check the transparency and encoding options if you are piping the result rather than writing a file. The project is Apache-2.0 licensed, the last push was on 2026-09-11, and the current release is v6.2.0 from 2026-07-13.
Frequently asked questions
How do I install node-html-to-image and what does it pull in?
With npm install node-html-to-image or yarn add node-html-to-image. The package depends on puppeteer, puppeteer-cluster and handlebars, and installing Puppeteer downloads a recent version of Chromium, roughly 170MB on Mac, 282MB on Linux and 280MB on Windows, which is guaranteed to work with the API. The package requires Node 22.12.0 or newer.
How do I make sure images and fonts are loaded before the screenshot is taken?
The waitUntil option defines when the markup is considered succeeded and accepts load and domcontentloaded, defaulting to load, and puppeteer no longer supports networkidle0 or networkidle2 for setContent. For more control there are two async hooks, beforeRendering which runs before setContent and gives access to the page for request interception or viewport changes, and beforeScreenshot which runs just before the screenshot is taken.
Can node-html-to-image use Handlebars templates?
Yes. If the content option is provided, the html property is treated as a Handlebars template and the content value fills it, with content accepting an object or an array. The readme also has a section on using Handlebars helpers, and Handlebars is a pinned dependency of the package.
How do I get the image as a buffer instead of a file?
Use the buffer instead of saving to disk option described in the readme, which suits a pipeline that pipes the result to object storage or into a response. The encoding option controls the representation, with binary as the default and base64 as the alternative, and the type option chooses jpeg or png with png as the default.
Can node-html-to-image render many images at once?
Yes, the readme has a section on generating multiple images, and the package depends on puppeteer-cluster, which is how the multiple-images feature runs. The puppeteer option also lets you substitute a different browser library such as puppeteer-core or puppeteer-extra, with puppeteerArgs carrying custom launch configuration.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/frinyvonnick-node-html-to-image)Was this useful?