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.
box-shadow, filter, object-fit, mix-blend-mode, background-blend-mode, border-image, repeating-linear-gradient(), writing-mode and zoom as unsupported, and transform as limited.toDataURL() throws) or are skipped, unless the server sends CORS headers and you set useCORS, or you run a proxy.| Option | Good for |
|---|---|
| html2canvas | Simple layouts, offline use, no server Re-implements CSS, so fidelity depends on what you use |
| html-to-image / dom-to-image | Better 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 |
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"
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.
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
}
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 | Shotmint parameter |
|---|---|
html2canvas(element) | selector (any CSS selector; first visible match) |
scale | device_scale_factor (1–3) |
backgroundColor: null | omit_background=true (PNG or WebP) |
windowWidth / windowHeight | width / height (viewport) |
ignoreElements, data-html2canvas-ignore | hide_selectors (comma-separated CSS selectors) |
useCORS, proxy | Not needed: images are loaded by the browser on our side |
canvas.toDataURL("image/jpeg", 0.9) | format=jpeg&quality=90 (or webp) |
localhost or a private network are blocked. Inline them as data URIs or serve them publicly.<canvas> chart with an <img src="canvas.toDataURL()"> in the clone before sending.cache_ttl=86400 and identical requests within a day are free.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.
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