DEV Community

Cover image for HTML to Image: How to Render HTML to PNG, JPG, or WebP
Nico Acosta for Grabbit

Posted on • Originally published at grabbit.live

HTML to Image: How to Render HTML to PNG, JPG, or WebP

To turn HTML into an image you have three real options: a client-side library that rasterizes a DOM node, an online converter for one-off jobs, and a screenshot API that renders your HTML at a URL and returns a hosted PNG, JPG, or WebP. The right one depends on whether the HTML is already on screen in a browser or whether you need to generate the image on a server, on demand, at scale.

This guide covers all three and shows where each breaks down.

The three ways to convert HTML to an image

Approach Runs where Best for Limitation
Client-side library (html-to-image, dom-to-image) In the browser Exporting a chart or card the user is viewing Cannot run on a server; complex CSS and cross-origin images break
Online converter (paste HTML) Their server One-off, manual conversions Manual, not automatable, not for production
Screenshot API (render a URL) Their server, via your code Automated server-side generation at scale Renders a URL, so host your HTML first

If the HTML is already rendered in front of a user, a client-side library is the fastest path. If you need to generate images from a server, on a schedule, or for thousands of pages, you want the API approach.

Option 1: Rasterize a DOM node in the browser

The html-to-image npm package generates an image from a DOM node using HTML5 canvas and SVG. It is the most-starred answer for the in-browser case:

import { toPng } from 'html-to-image';

const node = document.getElementById('invoice');
const dataUrl = await toPng(node);

const link = document.createElement('a');
link.download = 'invoice.png';
link.href = dataUrl;
link.click();
Enter fullscreen mode Exit fullscreen mode

This is excellent for "let the user download what they see," like exporting a generated chart, a receipt, or a shareable card. The catch is that it works only where a browser and the DOM already exist. It cannot render a URL from your backend, and because it reconstructs the node through canvas rather than using the browser's real renderer, web fonts, CSS filters, and cross-origin images frequently come out wrong or blank.

Option 2: Online converters

Tools like PDFCrowd, CloudConvert, and Templated let you paste HTML and download a PNG or JPG. They are fine for a one-time visual, but they are manual: there is no clean way to wire them into a build step, a webhook, or a per-user image pipeline. Use them to sanity-check a layout, not to generate images in production.

Option 3: Render your HTML at a URL and capture it with an API

For anything automated, the reliable pattern is to render your HTML at a real URL and screenshot it with an API that runs actual Chromium. This is how dynamic Open Graph images are generated: the API loads the page exactly like a browser, so your CSS, web fonts, and JavaScript all render correctly, and you get back a hosted image URL.

The key thing to understand: a URL-based screenshot API renders a URL, not a raw HTML string. So the first step is to host your HTML somewhere the API can reach it. That is usually a route in your own app.

Step 1: Host your HTML template at a route

Create a page that renders your HTML and reads any dynamic data from query parameters. Keep the body sized to the image you want:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <style>
      * { box-sizing: border-box; margin: 0; padding: 0; }
      body {
        width: 1200px;
        height: 630px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #0f172a;
        font-family: system-ui, sans-serif;
      }
      .title { color: #f8fafc; font-size: 64px; font-weight: 700; }
    </style>
  </head>
  <body>
    <div class="title">{{ title }}</div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Host it at a route like https://yoursite.com/card. Swap the placeholder for your framework's templating so the same page can render any title.

Step 2: Capture the rendered page

Point the screenshot API at that URL and pick your format:

curl https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/card?title=Ship+Faster",
    "width": 1200,
    "height": 630,
    "format": "webp"
  }'
Enter fullscreen mode Exit fullscreen mode

The response gives you a hosted image you can use directly:

{
  "id": "grb_01jx...",
  "status": "done",
  "image_url": "https://cdn.grabbit.live/grabs/grb_01jx....webp",
  "width": 1200,
  "height": 630,
  "format": "webp",
  "bytes": 31180,
  "execution_ms": 880
}
Enter fullscreen mode Exit fullscreen mode

Set "format" to png, jpeg, or webp depending on what you need (see the format section below). If your template draws content with JavaScript after load, add "delay_ms": 500 so the script finishes before the capture fires, or "selector": "#card" to wait until that element appears. To capture a tall page beyond the viewport, set "full_page": true.

Step 3: Cache the result

The image URL is stable, so store it the first time you generate it:

async function htmlToImage(title: string): Promise<string> {
  const url = new URL('https://yoursite.com/card');
  url.searchParams.set('title', title);

  const res = await fetch('https://api.grabbit.live/v1/grabs', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.GRABBIT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: url.toString(),
      width: 1200,
      height: 630,
      format: 'webp',
    }),
  });

  const grab = await res.json();
  return grab.image_url; // store this; reuse it on every later request
}
Enter fullscreen mode Exit fullscreen mode

Save image_url in your database or CMS and serve it from there. The API only runs again when the underlying content changes, so one template produces unlimited images and you pay once per unique image.

PNG vs JPG vs WebP: which format

  • PNG keeps text crisp and supports transparency. Best for UI, cards, and anything with sharp edges or a transparent background.
  • JPG (use "format": "jpeg") is smaller for photographic content but adds compression artifacts around text. Reach for it when the image is mostly a photo and file size matters.
  • WebP usually gives the smallest file at the same visual quality, which makes it a strong default for social cards and stored thumbnails. Every modern browser and social platform supports it.

All three come from the same render, so switching is a one-word change in the request body.

When to use which approach

Use the client-side html-to-image library when the content is already on screen and the user is exporting it themselves. Use an online converter for a quick one-off. Use a screenshot API whenever the generation needs to be automated, server-side, and reliable, because it renders in a real browser and removes the brittleness of canvas-based rasterization and the burden of running headless Chromium yourself.

For the closely related job of generating a unique social preview for every page, see how to generate dynamic OG images from any URL. To capture an existing live page rather than your own template, see how to screenshot a website from a URL.


Originally published on the Grabbit blog.

Top comments (0)