A Vue contact form that sends email

A Vue 3 component that posts your contact form to an endpoint, which stores each submission and emails you. Works in Vue and Nuxt without a server route or email API.

Free for 100 submissions a month, no credit card. Updated

Vue can bind a form, validate it and show a success message, but it can't send email from the browser. This component posts the fields to a Nisuform endpoint, which filters spam, saves the submission and emails you. It works in any Vue 3 app and in Nuxt, including a static Nuxt site.

The component

ContactForm.vue
<script setup lang="ts">
import { reactive, ref } from 'vue'

const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY'
const shownAt = Date.now()
const form = reactive({ name: '', email: '', message: '', _gotcha: '' })
const status = ref<'idle' | 'sending' | 'sent' | 'error'>('idle')
const error = ref('')

async function submit() {
  status.value = 'sending'
  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify({ ...form, _elapsed: String(Date.now() - shownAt) }),
    })
    const result = await response.json()
    if (result.ok) {
      status.value = 'sent'
    } else {
      error.value = result.message
      status.value = 'error'
    }
  } catch {
    error.value = 'Something went wrong. Please try again.'
    status.value = 'error'
  }
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks! We got your message.</p>
  <form v-else @submit.prevent="submit">
    <label>
      Name
      <input v-model="form.name" name="name" type="text" autocomplete="name" required />
    </label>
    <label>
      Email
      <input v-model="form.email" name="email" type="email" autocomplete="email" required />
    </label>
    <label>
      Message
      <textarea v-model="form.message" name="message" rows="5" required />
    </label>
    <input v-model="form._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" style="display: none" />
    <button type="submit" :disabled="status === 'sending'">
      {{ status === 'sending' ? 'Sending...' : 'Send' }}
    </button>
    <p v-if="status === 'error'" role="alert">{{ error }}</p>
  </form>
</template>

The reactive form object becomes the JSON body, and each key is the name its answer is saved under. Nothing resets form until the message goes through, so a failed submission keeps what the visitor typed.

Two fields feed the spam check. _gotcha is hidden, so only bots fill it in. _elapsed is how long the form was open, and anything sent in under two seconds is dropped.

Create a form and paste the key from its Setup tab into endpoint. The tab also has this component with your own field names under Code.

Sending files

On Pro, a submission can carry up to 5 images or PDFs. JSON can't hold files, so send FormData built from the form element instead:

Submitting with files
async function submit(event: Event) {
  const data = new FormData(event.target as HTMLFormElement)
  data.set('_elapsed', String(Date.now() - shownAt))
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: data,
  })
  const result = await response.json()
  error.value = result.ok ? '' : result.message
  status.value = result.ok ? 'sent' : 'error'
}

Give the file input a name, like <input type="file" name="attachment" />, and leave Content-Type unset so the browser can add the multipart boundary.

In Nuxt

Put the file in components/, or app/components/ in Nuxt 4, and Nuxt auto-imports it as <ContactForm />. The submit runs in the browser, so server/api stays empty and nuxt generate gives you a static site with a working form.

If you prefer $fetch, remember that it throws on a rejected submission. Catch the error and show error.data.message. To use a different endpoint per environment, put it in runtimeConfig.public and read it with useRuntimeConfig(). The Nuxt guide has a full Nuxt version.

What you get with Nisuform

Free covers 100 submissions a month across 3 forms. Pro is $10 a month for 5,000 submissions and adds the features marked Pro.

  • Spam filtering on every plan

    A honeypot, a fill-time check, rate limits and duplicate checks run on every submission, with optional Cloudflare Turnstile or Google reCAPTCHA. Spam never counts toward your limit.

  • An email for every submission

    You get an email within seconds of each submission. Reply-to is set to the visitor, so you can answer from your mail app.

  • Every submission kept

    Search, star and export to CSV. Submissions stay until you delete them, so a missed email never loses a message.

  • File uploadsPro

    Up to 5 images or PDFs per submission, stored privately and shared through links that expire.

  • Slack, Discord, Sheets and webhooksPro

    Send each submission to Slack, Discord, Telegram or Google Sheets, or anywhere else with signed webhooks, Zapier or Make.

  • Auto-repliesPro

    Send each visitor a confirmation in your own words, from your own sender name.

Vue guides

Frequently asked questions

Can a Vue app send email by itself?

No. Vue runs in the browser, and browsers can't send email. The form has to reach something that can, either your own server with an email provider or a form backend endpoint that stores the submission and emails you.

Does it work with Nuxt?

Yes, in Nuxt 3 and Nuxt 4. The form posts from the browser, so you don't need a server route, and it keeps working on a static site built with nuxt generate.

Should I use $fetch or fetch in Nuxt?

Either works. $fetch throws on any response that isn't a 2xx, so wrap it in try and catch and read the message from error.data.message. Plain fetch returns every response, which makes the message from a rejected submission easier to show.

Can I use VeeValidate or FormKit?

Yes. Let the library handle fields and validation, then send its values to your endpoint from its submit handler with Accept set to application/json.

Is the form key safe in my Vue code?

Yes. It only accepts new submissions. Nobody can read your submissions or change the form with it. Add your domains to the form's allowed websites to stop other sites from posting to it.

Start collecting form submissions

Set up your forms with built-in spam protection in minutes for free.

Jane Smith

jane@smith.design

Live

12.4k

Submissions

24

Forms

0

Spam