HTML to Image in React

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

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 or Remix 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,
    "credits_remaining": 95,
    "id": "abc123",
    "url": "https://i.html2img.com/abc123.png"
}

Use data.url directly as the src of an <img> - no blob or object URL handling required.

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 {
      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 {
      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 {
      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,
    }),
  });
  const { url } = await response.json();
  return Response.json({ url });
}

The returned url is hosted on i.html2img.com.