Skip to content
FormSubmit

Vue

How to build a Vue contact form with a form backend

A Vue 3 single-file component that submits to a hosted form backend with loading, success and error states — no server needed.

1 min readUpdated Sep 28, 2026

To build a Vue contact form without a backend, submit the form's FormData with fetch to a FormSubmit endpoint and track the status with a ref. FormSubmit emails you each submission, stores it, blocks spam and can forward it to Slack, Google Sheets or a webhook.

The component

src/components/ContactForm.vue:

src/components/ContactForm.vue
<script setup>
import { ref } from "vue";

const ENDPOINT = "https://formsubmit.app/f/YOUR_FORM_ID";
const status = ref("idle");
const error = ref("");
const renderedAt = Date.now();

async function onSubmit(e) {
  status.value = "sending";
  const data = new FormData(e.target);
  data.set("_ts", String(renderedAt));
  try {
    const res = await fetch(ENDPOINT, { method: "POST", body: data, headers: { Accept: "application/json" } });
    const json = await res.json();
    if (!res.ok || !json.ok) throw new Error(json.error?.message ?? "Something went wrong");
    status.value = "success";
    e.target.reset();
  } catch (err) {
    error.value = err.message;
    status.value = "error";
  }
}
</script>

<template>
  <p v-if="status === 'success'" class="text-green-700">Thanks! We'll be in touch soon.</p>
  <form v-else class="mx-auto max-w-lg space-y-5" @submit.prevent="onSubmit">
    <div class="space-y-1.5">
      <label for="fs-name" class="block text-sm font-medium text-gray-900">Name</label>
      <input id="fs-name" type="text" name="name" placeholder="Jane Doe" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
    </div>
    <div class="space-y-1.5">
      <label for="fs-email" class="block text-sm font-medium text-gray-900">Email</label>
      <input id="fs-email" type="email" name="email" placeholder="jane@example.com" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
    </div>
    <div class="space-y-1.5">
      <label for="fs-message" class="block text-sm font-medium text-gray-900">Message</label>
      <textarea id="fs-message" name="message" rows="5" placeholder="How can we help?" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"></textarea>
    </div>
    <!-- Honeypot: leave this hidden field empty to catch bots -->
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
    <p v-if="status === 'error'" class="text-sm text-red-600">{{ error }}</p>
    <button type="submit" :disabled="status === 'sending'" class="inline-flex w-full items-center justify-center rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800 disabled:opacity-50">
      {{ status === "sending" ? "Sending…" : "Send" }}
    </button>
  </form>
</template>

Walkthrough

  • @submit.prevent stops the full-page post and calls onSubmit.
  • new FormData(e.target) gathers every named field — no v-model needed for a simple form.
  • The _ts value is captured when the component is created so FormSubmit can detect bots that submit instantly.
  • Accept: application/json makes FormSubmit reply with { ok, id, message } or { ok: false, error }.

Using v-model

If you already bind inputs with v-model, send JSON:

js
await fetch(ENDPOINT, {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({ ...form.value, _ts: renderedAt }),
});

Validation

Native required, type="email" and maxlength attributes are enough for most contact forms. If you use VeeValidate or Vuelidate, run validation first and only call fetch when valid.

Next steps

Frequently asked questions

Does this work with Vite and Vue 3?

Yes. The component uses script setup and the Composition API.

What about Vue 2?

The same fetch logic works in an Options API component; move the refs into data and the function into methods.

Keep reading

Your form backend is 60 seconds away

Sign in with Google, create a form, paste the endpoint. Free forever for small sites — no credit card.