Home / Docs / Usage / HTML to Image in React

HTML to Image in React

Generate share images from a React app, server-side or on demand.

Prefer the official SDK?

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.

Common templates for React developers

React apps most often reach for these named templates: