Skip to content
FFormhook
All guides

Guide

Vue contact form (no backend needed)

Vue is a client-side framework, but the contact form you ship doesn't need a backend on your side. Use the plain HTML form action in any Vue template, or a Vue 3 SFC with <script setup> and fetch for an inline success/error UX.

Use a plain HTML form

You do not need an API route. The form POSTs directly to Formhook, and your framework never has to touch the submission.

contact.html
<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>

  <label>
    Message
    <textarea name="message" required></textarea>
  </label>

  <!-- honeypot: bots fill this; humans don't see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden />

  <!-- optional: send users to a thank-you page after a native POST -->
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />

  <button type="submit">Send</button>
</form>

Or submit with fetch in Vue

Use this version if you want client-side success/error states without a full page reload.

src/components/ContactForm.vue
<!-- src/components/ContactForm.vue -->
<script setup lang="ts">
import { ref } from "vue";

type Status = "idle" | "sending" | "ok" | "error";
const status = ref<Status>("idle");

async function onSubmit(e: Event) {
  e.preventDefault();
  status.value = "sending";

  const form = e.target as HTMLFormElement;
  const data = Object.fromEntries(new FormData(form));

  const res = await fetch("https://formhook.app/f/YOUR_API_KEY", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });

  status.value = res.ok ? "ok" : "error";
  if (res.ok) form.reset();
}
</script>

<template>
  <p v-if="status === 'ok'">Thanks — we'll be in touch.</p>
  <form v-else @submit="onSubmit">
    <input type="email" name="email" placeholder="Email" required />
    <textarea name="message" placeholder="Message" required />
    <input type="text" name="_gotcha" tabindex="-1" hidden />
    <button type="submit" :disabled="status === 'sending'">
      {{ status === "sending" ? "Sending…" : "Send" }}
    </button>
    <p v-if="status === 'error'">Something went wrong. Please try again.</p>
  </form>
</template>

How it works

The plain HTML form works inside any Vue template — it's just markup. For an inline success/error flow, the SFC snippet uses <script setup> with a ref-backed status state machine and a fetch call to formhook.app/f/YOUR_API_KEY. Object.fromEntries(new FormData(form)) serialized as JSON is the cleanest payload shape; Formhook also accepts form-encoded and multipart.

What happens next

  • Submission lands in your Formhook dashboard within a second.
  • Push notification fires on every device you've enabled it on.
  • Reply directly from the dashboard (Pro and above) — particularly useful for SPA-only deploys with no email-sending capability.

Ship your Vue contact form today.

Sign up free, create a form, paste the API key. Five minutes.

Start free

Free tier: 5 forms · 250 submissions/month · No credit card.

Vue contact form — FAQ

Do I need a backend or a Node server for this Vue contact form?
No. The form POSTs to Formhook directly, so your Vue app can stay a pure static SPA. Build with Vite, deploy the dist/ folder to any CDN.
Can I use this with Pinia / Vue Router state?
Yes — the snippet is a self-contained SFC, but you can lift the status state into Pinia or use Vue Router to navigate to a separate thank-you route after success.
What if I'm using Options API instead of <script setup>?
The same fetch pattern works in a methods.onSubmit handler with data() { return { status: 'idle' } }. Only the syntax differs — Formhook doesn't care which Vue style you use.

Guides for other frameworks