HTML to Image in React
Generate share images from a React app, server-side or on demand.
In your API routes and server actions, use the official JavaScript SDK, a zero-dependency typed client you install with npm install @html2img/client. The patterns below show the underlying HTTP calls.
Best for: React apps with server actions or API routes that proxy requests, plus client-side flows where the user generates an image and copies the URL. Always proxy through your server so the API key stays private.
Patterns covering server-side rendering inside Next.js actions, plus a client-side useEffect plus useCallback hook with cleanup.
API Response Format
Every successful request returns a JSON body with a url pointing to the generated image:
{
"success": true,
"id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402",
"expires_at": null,
"credits_remaining": 950,
"url": "https://i.html2img.com/image-1786092598870-921691.png"
}
expires_at is null on paid plans; on the free tier it carries an ISO 8601 timestamp - free-tier renders expire after 7 days, and upgrading makes existing renders permanent.
Use data.url directly as the src of an <img> - no blob or object URL handling required. On the free tier, renders expire after 7 days, so store a copy if the URL must outlive that.
Basic Component Example
Here’s a React component that generates images from HTML:
import { useState } from 'react';
function ImageGenerator() {
const [imageUrl, setImageUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const generateImage = async () => {
setLoading(true);
setError(null);
try {
// Local demo only - in production, proxy through a server route so the key never ships to the browser
const response = await fetch('https://app.html2img.com/api/html', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.REACT_APP_HTML2IMG_API_KEY
},
body: JSON.stringify({
html: `
<div style="padding: 20px; background: #f0f0f0;">
<h1>Hello from React!</h1>
<p>Generated at ${new Date().toLocaleString()}</p>
</div>
`,
width: 800,
height: 600
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Failed to generate image');
}
setImageUrl(data.url);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
return (
<div>
<button
onClick={generateImage}
disabled={loading}
>
{loading ? 'Generating...' : 'Generate Image'}
</button>
{error && (
<div style={{ color: 'red' }}>
Error: {error}
</div>
)}
{imageUrl && (
<div>
<img
src={imageUrl}
alt="Generated content"
style={{ maxWidth: '100%' }}
/>
</div>
)}
</div>
);
}
export default ImageGenerator;
Screenshot Component
Here’s a component for taking screenshots:
import { useState } from 'react';
function ScreenshotGenerator() {
const [url, setUrl] = useState('https://example.com');
const [imageUrl, setImageUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const takeScreenshot = async () => {
setLoading(true);
setError(null);
try {
// Local demo only - in production, proxy through a server route so the key never ships to the browser
const response = await fetch('https://app.html2img.com/api/screenshot', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.REACT_APP_HTML2IMG_API_KEY
},
body: JSON.stringify({
url,
width: 1200,
height: 800,
dpi: 2,
fullpage: true
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Failed to take screenshot');
}
setImageUrl(data.url);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
return (
<div>
<div>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Enter URL"
style={{ width: '100%', marginBottom: '1rem' }}
/>
</div>
<button
onClick={takeScreenshot}
disabled={loading}
>
{loading ? 'Taking Screenshot...' : 'Take Screenshot'}
</button>
{error && (
<div style={{ color: 'red', margin: '1rem 0' }}>
Error: {error}
</div>
)}
{imageUrl && (
<div style={{ marginTop: '1rem' }}>
<img
src={imageUrl}
alt="Screenshot"
style={{ maxWidth: '100%' }}
/>
</div>
)}
</div>
);
}
export default ScreenshotGenerator;
Custom Hook Example
Here’s a custom hook to reuse the image generation logic:
import { useState, useCallback } from 'react';
function useHtml2Img() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const generateImage = useCallback(async (html, options = {}) => {
setLoading(true);
setError(null);
try {
// Local demo only - in production, proxy through a server route so the key never ships to the browser
const response = await fetch('https://app.html2img.com/api/html', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.REACT_APP_HTML2IMG_API_KEY
},
body: JSON.stringify({
html,
width: options.width || 800,
height: options.height || 600,
...options
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Failed to generate image');
}
return data.url;
} catch (error) {
setError(error.message);
throw error;
} finally {
setLoading(false);
}
}, []);
return {
generateImage,
loading,
error
};
}
// Usage Example
function MyComponent() {
const { generateImage, loading, error } = useHtml2Img();
const [imageUrl, setImageUrl] = useState(null);
const handleGenerate = async () => {
try {
const url = await generateImage(`
<div style="padding: 20px; background: #f0f0f0;">
<h1>Hello from Custom Hook!</h1>
<p>Generated at ${new Date().toLocaleString()}</p>
</div>
`);
setImageUrl(url);
} catch (error) {
console.error('Failed to generate image:', error);
}
};
return (
<div>
<button onClick={handleGenerate} disabled={loading}>
Generate Image
</button>
{error && <div style={{ color: 'red' }}>Error: {error}</div>}
{imageUrl && <img src={imageUrl} alt="Generated content" />}
</div>
);
}
Environment Setup
Make sure to set up your environment variables:
REACT_APP_HTML2IMG_API_KEY=your-api-key
For Vite-based React projects, use:
VITE_HTML2IMG_API_KEY=your-api-key
And access it with:
const apiKey = import.meta.env.VITE_HTML2IMG_API_KEY;
Never expose your API key in client-side code. Proxy requests through your backend, including Next.js API routes or server actions.
Common patterns
useEffect plus useCallback with cleanup
import { useCallback, useEffect, useState } from 'react';
export function useShareImage(html, options = {}) {
const [url, setUrl] = useState(null);
const [error, setError] = useState(null);
const render = useCallback(async (signal) => {
try {
// POST to your own API route that holds the API key
const response = await fetch('/api/share-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html, ...options }),
signal,
});
if (!response.ok) throw new Error(`Status ${response.status}`);
const data = await response.json();
setUrl(data.url);
} catch (err) {
if (err.name !== 'AbortError') setError(err);
}
}, [html, options]);
useEffect(() => {
const controller = new AbortController();
render(controller.signal);
return () => controller.abort();
}, [render]);
return { url, error };
}
The AbortController cancels the in-flight request when inputs change or the component unmounts.
Templates in React
Use a named template when your data is structured. Here is the Invoice Image template, called from a Next.js Route Handler:
// app/api/render-invoice/route.ts
export async function POST(request: Request) {
const body = await request.json();
const response = await fetch('https://app.html2img.com/api/v1/templates/invoice-image', {
method: 'POST',
headers: { 'X-API-Key': process.env.HTML2IMG_API_KEY!, 'Content-Type': 'application/json' },
body: JSON.stringify({
invoice_number: body.number,
business_name: 'Coastline Coffee Co',
client_name: body.client,
items: body.items,
total: body.total,
}),
});
if (!response.ok) {
const error = await response.json();
return Response.json(error, { status: response.status });
}
const { url } = await response.json();
return Response.json({ url });
}
The returned url is hosted on i.html2img.com.
Rendering a PDF instead
The same request shape produces a PDF. From a server route, forward format: 'pdf' and hand the URL to your component:
export async function POST(request) {
const { html } = await request.json();
const response = await fetch('https://app.html2img.com/api/html', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.HTML2IMG_API_KEY
},
body: JSON.stringify({ html, format: 'pdf' })
});
const { url } = await response.json(); // ends in .pdf
return Response.json({ url });
}
A PDF cannot go in an <img> tag, so render a download link instead. The document is vector with selectable text, on A4 portrait pages that paginate automatically. See the format parameter docs and the HTML to PDF API overview.
Related guides and articles
- JavaScript SDK - the official typed npm client, ideal for your API routes and server actions
- Browse all templates
- Open Graph Image template
- Code Screenshot template
- Articles index
- Dynamic OG images in Next.js without vercel/og
- Getting started with the API
- Pricing
Common templates for React developers
React apps most often reach for these named templates: