HTML to Image in Vue

Generate share images from a Vue or Nuxt app, with the Composition API.

Best for: Vue 3 apps and Nuxt server routes. The Composition API hook below works in both contexts. Always proxy through your server so the API key stays private.

Patterns covering Composition API with onMounted, plus a Nuxt server route example for templates.

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"
}

Bind data.url straight to an <img :src> - no blob or object URL handling required.

Basic Component Example

Here’s a Vue component that generates images from HTML:

<template>
  <div>
    <button
      @click="generateImage"
      :disabled="loading"
    >
      {{ loading ? 'Generating...' : 'Generate Image' }}
    </button>

    <div v-if="error" class="error">
      Error: {{ error }}
    </div>

    <div v-if="imageUrl">
      <img
        :src="imageUrl"
        alt="Generated content"
        class="generated-image"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const imageUrl = ref(null);
const loading = ref(false);
const error = ref(null);

const generateImage = async () => {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('https://app.html2img.com/api/html', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': import.meta.env.VITE_HTML2IMG_API_KEY
      },
      body: JSON.stringify({
        html: `
          <div style="padding: 20px; background: #f0f0f0;">
            <h1>Hello from Vue!</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');
    }

    imageUrl.value = data.url;
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped>
.error {
  color: red;
  margin: 1rem 0;
}

.generated-image {
  max-width: 100%;
  margin-top: 1rem;
}
</style>

Screenshot Component

Here’s a component for taking screenshots:

<template>
  <div>
    <div class="input-group">
      <input
        v-model="url"
        type="text"
        placeholder="Enter URL"
        class="url-input"
      />
    </div>

    <button
      @click="takeScreenshot"
      :disabled="loading"
      class="screenshot-button"
    >
      {{ loading ? 'Taking Screenshot...' : 'Take Screenshot' }}
    </button>

    <div v-if="error" class="error">
      Error: {{ error }}
    </div>

    <div v-if="imageUrl" class="image-container">
      <img
        :src="imageUrl"
        alt="Screenshot"
        class="screenshot-image"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const url = ref('https://example.com');
const imageUrl = ref(null);
const loading = ref(false);
const error = ref(null);

const takeScreenshot = async () => {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('https://app.html2img.com/api/screenshot', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': import.meta.env.VITE_HTML2IMG_API_KEY
      },
      body: JSON.stringify({
        url: url.value,
        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');
    }

    imageUrl.value = data.url;
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped>
.input-group {
  margin-bottom: 1rem;
}

.url-input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.screenshot-button {
  margin-bottom: 1rem;
}

.error {
  color: red;
  margin: 1rem 0;
}

.image-container {
  margin-top: 1rem;
}

.screenshot-image {
  max-width: 100%;
  border: 1px solid #eee;
  border-radius: 4px;
}
</style>

Composable Example

Here’s a composable function to reuse the image generation logic:

// useHtml2Img.js
import { ref } from 'vue';

export function useHtml2Img() {
  const loading = ref(false);
  const error = ref(null);

  const generateImage = async (html, options = {}) => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch('https://app.html2img.com/api/html', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': import.meta.env.VITE_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 (err) {
      error.value = err.message;
      throw err;
    } finally {
      loading.value = false;
    }
  };

  return {
    generateImage,
    loading,
    error
  };
}

// Usage Example
<template>
  <div>
    <button
      @click="handleGenerate"
      :disabled="loading"
    >
      Generate Image
    </button>

    <div v-if="error" class="error">
      Error: {{ error }}
    </div>

    <div v-if="imageUrl">
      <img :src="imageUrl" alt="Generated content" />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useHtml2Img } from './useHtml2Img';

const { generateImage, loading, error } = useHtml2Img();
const imageUrl = ref(null);

const handleGenerate = async () => {
  try {
    imageUrl.value = await generateImage(`
      <div style="padding: 20px; background: #f0f0f0;">
        <h1>Hello from Composable!</h1>
        <p>Generated at ${new Date().toLocaleString()}</p>
      </div>
    `);
  } catch (err) {
    console.error('Failed to generate image:', err);
  }
};
</script>

Environment Setup

Create a .env file in your project root:

VITE_HTML2IMG_API_KEY=your-api-key

Access the environment variable in your components:

const apiKey = import.meta.env.VITE_HTML2IMG_API_KEY;

Never expose your API key in client-side code. Use environment variables and consider proxying requests through your backend.

These examples use the Composition API with <script setup>, the recommended approach for Vue 3. Adapt to the Options API for older Vue 2 codebases.

Common patterns

Composition API with onMounted

<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({ html: String });
const url = ref(null);
const error = ref(null);

onMounted(async () => {
  try {
    const response = await fetch('/api/share-image', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ html: props.html }),
    });
    if (!response.ok) throw new Error(`Status ${response.status}`);
    const data = await response.json();
    url.value = data.url;
  } catch (e) {
    error.value = e;
  }
});
</script>

<template>
  <img v-if="url" :src="url" alt="Generated share image" />
  <p v-else-if="error">Failed to render</p>
  <p v-else>Rendering...</p>
</template>

Nuxt server route

// server/api/render-invoice.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  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: {
      invoice_number: body.number,
      business_name: 'Coastline Coffee Co',
      client_name: body.client,
      items: body.items,
      total: body.total,
    },
  });
  return { url: response.url };
});

Templates in Vue

Use a named template when your data is structured. Here is the Invoice Image template, called from a Nuxt server route:

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: 'INV-2026-0042',
    business_name: 'Coastline Coffee Co',
    client_name: 'Riverside Bakery',
    items: [{ description: 'Wholesale beans', quantity: '50', unit_price: '$15.00', amount: '$750.00' }],
    total: '$750.00',
  }),
});
const { url } = await response.json();

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

Common templates for Vue developers

Vue apps most often reach for these named templates: