How to Convert HTML to JPG in React
Render a React element with html2canvas, encode it as a JPEG, and handle CORS, fonts, large pages, and server-side alternatives.
Use html2canvas to render the React element into a canvas, encode that canvas as JPEG, and trigger a download. Run the capture after React has rendered the element and after its images and fonts are ready.
1. Client-side React: HTML element to JPG
Install the browser library:
npm install html2canvas
This component captures one element, uses the device pixel ratio for sharper output, and writes a quality-controlled JPEG:
import { useRef, useState } from 'react';
import html2canvas from 'html2canvas';
export function DownloadCard() {
const cardRef = useRef(null);
const [busy, setBusy] = useState(false);
async function downloadJpg() {
if (!cardRef.current || busy) return;
setBusy(true);
try {
const images = Array.from(cardRef.current.querySelectorAll('img'));
await Promise.all(images.map((img) => img.complete ? Promise.resolve() : new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
})));
if (document.fonts?.ready) await document.fonts.ready;
const canvas = await html2canvas(cardRef.current, {
scale: window.devicePixelRatio,
useCORS: true,
backgroundColor: '#ffffff',
});
const link = document.createElement('a');
link.download = 'card.jpg';
link.href = canvas.toDataURL('image/jpeg', 0.92);
link.click();
} finally {
setBusy(false);
}
}
return <>Content to export
This rendered React component becomes a JPG.
>;
}
toDataURL('image/jpeg', 0.92) uses the browser canvas JPEG encoder. Quality ranges from 0 to 1. JPEG is lossy and has no transparency; choose PNG for transparent pixels or lossless text and line art.
Capture options
| Option | Use | Trade-off |
|---|---|---|
scale |
Increase output resolution; device pixel ratio is a useful default. | More memory and a larger file. |
useCORS: true |
Requests images with CORS enabled. | The image server must send an allowing CORS header. |
backgroundColor |
Sets a solid background for JPEG. | JPEG cannot preserve transparency. |
logging |
Enable diagnostics while investigating a render. | Disable it for normal downloads. |
ignoreElements |
Skip controls such as the export button. | Skipped nodes are absent from the image. |
2. Make the output match the component
- Capture the exact element whose ref you own rather than
document.body. - Apply export-only CSS for a fixed width, white background, and predictable spacing.
- Wait for images and
document.fonts.ready. - Keep the element mounted and visible while capture runs.
html2canvas reconstructs an image from DOM and CSS information; it is not a native browser screenshot. Unsupported CSS or effects can differ from the visible page.
3. Images, fonts, and cross-origin content
Remote images can taint the canvas or be omitted. The image host must return an appropriate Access-Control-Allow-Origin header. useCORS is only a request hint and cannot relax browser security.
- Prefer same-origin assets or a proxy restricted to approved hosts.
- Do not build an unrestricted URL-fetch proxy because it creates an SSRF risk.
- Cross-origin iframes cannot be read by html2canvas.
- Inline critical SVG and preload fonts when deterministic output matters.
4. Long pages and canvas limits
Very large elements can exceed browser canvas dimensions or memory. Limits vary by browser, platform, and hardware, so there is no universal safe maximum. Blank output, clipped content, and allocation errors are common symptoms.
Reduce scale, split the document into sections and stitch the JPEGs, or move the job to a real browser on a server.
5. Playwright or Puppeteer for real browser rendering
Use browser automation for actual browser painting, full-page screenshots, server-side jobs, or content html2canvas cannot access. Playwright supports JPEG screenshots and quality settings:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 92, fullPage: true });
await browser.close();
This requires browser binaries and infrastructure. Reuse browser processes, limit concurrency, and set navigation and asset timeouts.
6. Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API and MCP server. One request returns PNG, JPEG, WebP, or PDF. It supports full-page or CSS-element capture, lazy-image loading, dark mode, device presets, custom viewport and retina scale, waits, custom CSS and JavaScript, clicks, hidden selectors, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API.
Cookie banners, newsletter popups, and chat widgets are removed before capture. See the ScreenshotNeo API documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Each response reports the result in X-Page-Verdict and X-Billed headers. The MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with 1,000 screenshots each month.
7. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
SecurityError or blank image |
Cross-origin image tainted the canvas. | Serve it with CORS, move it same-origin, or use a controlled proxy. |
| Images missing | Capture ran before image load or CORS was denied. | Await image load and verify response headers. |
| Fonts/layout differ | Fonts were not ready or CSS was unsupported. | Await document.fonts.ready, simplify export CSS, or switch to Playwright. |
| Output clipped or blank | Canvas dimensions or memory were exceeded. | Lower scale, split captures, or use a server browser. |
| Download does nothing | Browser download restrictions. | Trigger the click from the user gesture and keep the anchor in the document briefly if needed. |
| Iframe content absent | Cross-origin iframe isolation. | Capture inside that origin or use Playwright/ScreenshotNeo. |
8. Performance, reliability, and cost
- Client: work scales with pixel area; high DPR and long pages consume memory. Disable duplicate clicks.
- Server browser: reuse a browser process, limit concurrent pages, configure timeouts, and retry only idempotent captures.
- Determinism: freeze animations, use fixed viewport and timezone, and wait for a known selector or network idle.
- Cost: html2canvas has no API charge but uses client CPU and memory. Playwright and Puppeteer require browser infrastructure. ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits cost nothing.
9. FAQ
Can I convert arbitrary HTML strings?
Render the string into a mounted React element first, then capture it. Sanitize untrusted HTML.
Does JPEG preserve transparency?
No. Set a background color or choose PNG.
Can html2canvas run in Node.js?
No. Use Playwright, Puppeteer, or a hosted capture API for Node.js jobs.
Why is the result not pixel-identical?
DOM reconstruction supports only the CSS it understands. Use a real browser screenshot for exact visual output.


