html2canvas Alternative

Renderfy vs html2canvas

html2canvas does not screenshot your page. It re-implements CSS painting in JavaScript and draws its best approximation onto a canvas. Every bug you have filed against it — the oklch crash, the ignored z-index, the missing shadow — is that one design decision surfacing.

Read this before you migrate

Renderfy is not a screenshot API. It renders markup you send it; it does not load a URL and capture a live page. If your html2canvas call captures a live, interactive, or third-party page, a screenshot service or Puppeteer is the right replacement — not this. If it captures content you already generate — a receipt, a chart, a share card, a certificate — keep reading.

Why the oklch crash is not a bug you can wait out

Tailwind CSS v4 moved its default palette to the oklch() colour space. html2canvas ships its own CSS colour parser, that parser has no oklch support, and it throws instead of degrading:

Error: Attempting to parse an unsupported color function "oklch"

The available fixes are all the same shape — maintain a second, downgraded colour pipeline purely so the export path can cope:

  • Swap in the html2canvas-pro fork and take on a second dependency's maintenance risk.
  • Add a PostCSS step converting oklch to rgb, losing the wider gamut your design system chose on purpose.
  • Rewrite colours to rgb fallbacks at runtime before the capture and restore them afterwards.

A server-side renderer has none of this class of problem, because it is not guessing at CSS — Chromium already implements it.

Feature comparison

Feature-by-feature comparison of html2canvas and Renderfy
Featurehtml2canvasRenderfy
Where it runsIn the visitor's browser, on their CPUOn our servers — one HTTPS POST
How the image is producedRe-implements CSS painting onto a <canvas> from the DOMRenders your markup server-side (Satori for images, Chromium for PDF)
Modern colour functionsThrows on oklch() — the Tailwind v4 default paletteoklch, color-mix and lab render natively on the Chromium PDF path
z-index / stacking contextsIgnored — elements paint in DOM orderHonoured on the Chromium path
Cross-origin imagesTaint the canvas unless proxied or CORS-enabledFetched server-side, no browser CORS involved
DeterminismDepends on browser, OS, devicePixelRatio and installed fontsIdentical bytes for identical input
FontsWhatever happens to be loaded on the visitor's pageGoogle Font family names passed per request
Client bundle costA rendering library shipped to every visitorZero — nothing runs on the client
Output formatsCanvas → PNG or JPEG data URLPNG, JPEG, WebP, PDF
Input typesA live DOM nodeTailwind, HTML, Markdown, code, charts, diagrams, LaTeX math
Screenshotting a live, interactive pageYes — that is its nicheNo — send markup, not a URL
PriceFree (MIT)Credit-based from $9/mo — 1 credit per image

The migration

Before — in the browser

import html2canvas from "html2canvas";

async function saveCard() {
  const el = document.getElementById("card")!;
  const canvas = await html2canvas(el, {
    scale: window.devicePixelRatio,
    useCORS: true,
  });
  const url = canvas.toDataURL("image/png");
  triggerDownload(url, "card.png");
}

// Throws on Tailwind v4 oklch colours.
// z-index ignored. Output differs per device.

After — on the server

// app/api/card/route.ts
export async function POST(req: Request) {
  const { title } = await req.json();

  const res = await fetch(
    "https://renderfy.io/api/v1/render",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.RENDERFY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        type: "tailwind",
        content: `<div class="flex h-full w-full items-center
                     justify-center bg-zinc-950">
                     <h1 class="text-5xl font-bold text-zinc-50">
                       ${title}
                     </h1>
                   </div>`,
        options: {
          format: "png",
          dimensions: { width: 1200, height: 630 },
        },
      }),
    }
  );

  return new Response(await res.arrayBuffer(), {
    headers: {
      "Content-Type": "image/png",
      "Content-Disposition":
        'attachment; filename="card.png"',
    },
  });
}

The download button stays. What changes is that it calls your endpoint instead of doing the rendering on the visitor's machine — so support stops receiving “the export looks wrong on my laptop” tickets.

When to keep html2canvas

  • You genuinely need to capture live, user-manipulated DOM state — a drag-and-drop canvas, an annotated screenshot, a whiteboard.
  • The content must never leave the user's device.
  • The export is cosmetic, low-stakes, and the approximation is good enough.

Frequently asked questions

Why does html2canvas throw "Attempting to parse an unsupported color function oklch"?+

Because Tailwind CSS v4 ships its default palette in the oklch() colour space, and html2canvas has its own CSS colour parser that predates it. The parser does not understand oklch, so it throws rather than falling back. It is not a Tailwind bug and not a browser bug — it is the consequence of a library reimplementing CSS instead of using the browser's own painter. Workarounds exist (the html2canvas-pro fork, a PostCSS oklch-to-rgb transform, or CSS custom property fallbacks), but each one means shipping a second, downgraded colour pipeline just for exports.

Why does html2canvas ignore z-index?+

It walks the DOM and paints elements in document order rather than resolving stacking contexts the way a browser compositor does. Overlapping badges, sticky headers, modals and dropdowns therefore come out layered wrong. There is no option to fix it; it is inherent to the approach.

Is html2canvas a screenshot library?+

No. Its own documentation is upfront that the output is a reconstruction rather than a capture: the library reads what it can from the DOM and computed styles and repaints that onto a canvas. Browsers do not expose a real screenshot API to page JavaScript, so an approximation is the only thing a library in that position can offer. Almost everything reported as an html2canvas bug is a gap between that approximation and the browser's own painter.

Is Renderfy a screenshot API?+

No — and it matters that you know before signing up. Renderfy renders markup you send it; it does not visit a URL and capture a live page. If you need to capture an authenticated dashboard, a third-party site or a page with running JavaScript, you want a screenshot API or Puppeteer instead. If you need a deterministic image of content you already control — a share card, a chart, a certificate, a receipt — Renderfy is the better fit because there is no browser session to reproduce.

Does Renderfy support CSS grid for images?+

Not on the image path. Renderfy renders PNG, JPEG and WebP with Satori, which supports flexbox and absolute positioning but not display: grid — the same constraint that applies to @vercel/og. The PDF path is a real headless Chromium and supports grid, CSS variables, oklch and @media print in full. Choose the path by what the layout needs.

What replaces the html2canvas download button?+

The button now calls your own endpoint instead of rendering locally. Your route handler builds the same markup on the server, POSTs it to Renderfy, and streams the PNG back with a Content-Disposition header. You delete the html2canvas dependency, the visitor's device stops doing the work, and every user gets identical output.