html2canvas alternative: real-browser element screenshots

html2canvas doesn't actually take a screenshot. It walks the DOM and re-implements CSS painting on a <canvas>, so anything it hasn't re-implemented is drawn wrong or not at all. When the image has to match what users see, render the same HTML in real Chromium instead.

Why html2canvas output looks wrong

Your options

OptionGood for
html2canvasSimple layouts, offline use, no server
Re-implements CSS, so fidelity depends on what you use
html-to-image / dom-to-imageBetter CSS coverage: the browser itself paints the DOM via an SVG <foreignObject>
Still needs CORS for images and inlines fonts; Safari has quirks
Headless Chromium on a server (Puppeteer, Playwright, or a hosted API like Shotmint)Pixel-perfect output, any CSS, any image origin, retina scale, PDFs too
Needs a network round trip; self-hosting means running Chromium

If the page is public: send the URL and a selector

The simplest case. selector crops the screenshot to the first matching element, and device_scale_factor=2 gives a retina image.

curl -H "X-API-Key: $SHOTMINT_KEY" -o infobox.png \
  "https://shotmint.creasework.com/v1/screenshot?url=https://en.wikipedia.org/wiki/HTML&selector=.infobox&device_scale_factor=2"

If the content only exists in the user's browser: send the HTML

Dashboards behind a login, a chart the user just configured, a card they designed: serialize the current DOM in the browser, send it to your backend, and let the backend render it. The <base> tag makes relative CSS and image URLs resolve against your site, and scripts are removed so a SPA doesn't re-render over the snapshot.

Browser

async function captureElement(selector) {
  const live = document.querySelectorAll("input, textarea, select");
  const clone = document.documentElement.cloneNode(true);
  // outerHTML doesn't include what the user typed, so copy current values into attributes
  clone.querySelectorAll("input, textarea, select").forEach((el, i) => {
    const v = live[i];
    if (el.type === "checkbox" || el.type === "radio") el.toggleAttribute("checked", v.checked);
    else if (el.tagName === "TEXTAREA") el.textContent = v.value;
    else if (el.tagName === "SELECT") el.querySelectorAll("option").forEach((o, j) => o.toggleAttribute("selected", j === v.selectedIndex));
    else el.setAttribute("value", v.value);
  });
  clone.querySelectorAll("script").forEach(s => s.remove());
  const baseTag = document.createElement("base");
  baseTag.href = location.href;
  clone.querySelector("head").prepend(baseTag);

  const r = await fetch("/api/capture", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ html: "<!doctype html>" + clone.outerHTML, selector, width: innerWidth }),
  });
  if (!r.ok) throw new Error(await r.text());
  return URL.createObjectURL(await r.blob()); // use as <img src> or a download link
}

Backend (Node.js / Express)

Keeps your API key on the server. Add your own auth so strangers can't spend your credits.

import express from "express";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/api/capture", async (req, res) => {
  const { html, selector, width } = req.body;
  const r = 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,
      width: Math.min(Number(width) || 1280, 1920),
      device_scale_factor: 2,
      format: "png",
    }),
  });
  res.status(r.status).type(r.headers.get("content-type")).send(Buffer.from(await r.arrayBuffer()));
});

app.listen(3000);

html2canvas options, mapped

html2canvasShotmint parameter
html2canvas(element)selector (any CSS selector; first visible match)
scaledevice_scale_factor (1–3)
backgroundColor: nullomit_background=true (PNG or WebP)
windowWidth / windowHeightwidth / height (viewport)
ignoreElements, data-html2canvas-ignorehide_selectors (comma-separated CSS selectors)
useCORS, proxyNot needed: images are loaded by the browser on our side
canvas.toDataURL("image/jpeg", 0.9)format=jpeg&quality=90 (or webp)

Gotchas

What it costs

One render = one credit: 100 free with a free key, then €5 for 2,000 or €19 for 10,000, with no subscription and credits that never expire. Failed renders are refunded. Just want to try a snippet by hand? The HTML to image converter renders pasted HTML and CSS in the same Chromium.

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