Vue Contact Form with Validation (Vue 3 and Vue 2)
Build a Vue contact form that posts to a Formboost endpoint with fetch — reactive state, validation, loading and success states, a honeypot, no backend.
A Vue app renders the form; it has nowhere to send it. This guide builds a contact form in Vue 3
with <script setup> — and the same thing in the Options API for Vue 2 — that posts JSON to a
Formboost endpoint and shows loading, error and success states. No API route, no
server, no email library.
Key takeaways
- The component posts JSON with
fetchand reads the202response; the visitor never leaves the page.- Browser validation (
required,type="email") runs first, so the handler only sees complete input.- A hidden
_honeyfield catches bots;_replytomakes the notification email replyable.- Vue 2 needs only the Options API version below — the request is identical.
1. Create the endpoint
Sign in to the dashboard, create a form, and copy its
endpoint ID. The URL you post to is https://formboost.app/f/YOUR_ENDPOINT_ID. Email
notifications to your address are on by default.
2. The component (Vue 3, <script setup>)
1<!-- src/components/ContactForm.vue -->
2<template>
3 <form @submit.prevent="submit">
4 <label>
5 Name
6 <input v-model.trim="form.name" type="text" required />
7 </label>
8
9 <label>
10 Email
11 <input v-model.trim="form._replyto" type="email" required />
12 </label>
13
14 <label>
15 Message
16 <textarea v-model.trim="form.message" rows="5" required></textarea>
17 </label>
18
19 <!-- Honeypot: hidden from people, filled by bots. Leave it empty. -->
20 <input
21 v-model="form._honey"
22 type="text"
23 name="_honey"
24 style="display: none"
25 tabindex="-1"
26 autocomplete="off"
27 />
28
29 <button type="submit" :disabled="status === 'sending'">
30 {{ status === 'sending' ? 'Sending…' : 'Send message' }}
31 </button>
32
33 <p v-if="status === 'sent'" role="status">Thanks — we'll reply to {{ sentTo }} soon.</p>
34 <p v-if="status === 'error'" role="alert">{{ error }}</p>
35 </form>
36</template>
37
38<script setup>
39import { reactive, ref } from 'vue';
40
41const ENDPOINT = 'https://formboost.app/f/YOUR_ENDPOINT_ID';
42
43const form = reactive({
44 name: '',
45 _replyto: '',
46 message: '',
47 _honey: '',
48 _subject: 'New message from the contact form',
49});
50
51const status = ref('idle'); // idle | sending | sent | error
52const error = ref('');
53const sentTo = ref('');
54
55async function submit() {
56 status.value = 'sending';
57 error.value = '';
58
59 try {
60 const res = await fetch(ENDPOINT, {
61 method: 'POST',
62 headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
63 body: JSON.stringify(form),
64 });
65
66 if (!res.ok) {
67 // Every 4xx carries a machine-readable `name` and a hint.
68 const body = await res.json().catch(() => ({}));
69 throw new Error(body.message || `Request failed (${res.status})`);
70 }
71
72 sentTo.value = form._replyto;
73 status.value = 'sent';
74 Object.assign(form, { name: '', _replyto: '', message: '', _honey: '' });
75 } catch (err) {
76 status.value = 'error';
77 error.value = err.message || 'Something went wrong. Please try again.';
78 }
79}
80</script>Drop it into a page:
1<script setup>
2import ContactForm from './components/ContactForm.vue';
3</script>
4
5<template>
6 <main>
7 <h1>Contact</h1>
8 <ContactForm />
9 </main>
10</template>Formboost answers a JSON request with 202 Accepted and a body like
{"success":true,"message":"Submission accepted for processing","requestId":"…"}. The
submission is queued, stored, screened for spam and delivered in the background; the
requestId identifies it if you ever need to ask support about it.
3. What each reserved field does
| Field | Effect |
|---|---|
_replyto | Sets the reply-to on the notification email, so replying from your inbox answers the visitor. It is also the natural field to pick as the recipient of an auto-reply. |
_subject | The notification email's subject line (up to 200 characters). |
_honey | The honeypot. A submission with this field filled is stored as spam and never delivered. |
_redirect | Not used here — a fetch request is never redirected. Only a plain HTML post follows it. |
Every other field name — name, message, anything you add — becomes a column in the
dashboard, a key under submission in a webhook payload, and a column
in the CSV export.
4. Validation
Browser validation does most of the work: required and type="email" stop the submit
before your handler runs, so the handler only sees complete input. For messages you want to
check yourself — a minimum length, say — validate in submit() before calling fetch:
1if (form.message.length < 20) {
2 status.value = 'error';
3 error.value = 'Please write a little more so we can help.';
4 return;
5}There is nothing to validate server-side for a contact form; Formboost stores what it receives, and the spam screen scores it. If you need rules the endpoint does not enforce, they belong in the component.
Vue 2 (Options API)
The request is the same; only the component syntax changes.
1<template>
2 <form @submit.prevent="submit">
3 <input v-model.trim="form.name" type="text" placeholder="Name" required />
4 <input v-model.trim="form._replyto" type="email" placeholder="Email" required />
5 <textarea v-model.trim="form.message" placeholder="Message" required></textarea>
6 <input v-model="form._honey" type="text" style="display:none" tabindex="-1" autocomplete="off" />
7 <button type="submit" :disabled="status === 'sending'">
8 {{ status === 'sending' ? 'Sending…' : 'Send message' }}
9 </button>
10 <p v-if="status === 'sent'">Thanks — we'll be in touch.</p>
11 <p v-if="status === 'error'">{{ error }}</p>
12 </form>
13</template>
14
15<script>
16export default {
17 data() {
18 return {
19 form: { name: '', _replyto: '', message: '', _honey: '' },
20 status: 'idle',
21 error: '',
22 };
23 },
24 methods: {
25 async submit() {
26 this.status = 'sending';
27 try {
28 const res = await fetch('https://formboost.app/f/YOUR_ENDPOINT_ID', {
29 method: 'POST',
30 headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
31 body: JSON.stringify(this.form),
32 });
33 if (!res.ok) throw new Error(`Request failed (${res.status})`);
34 this.status = 'sent';
35 this.form = { name: '', _replyto: '', message: '', _honey: '' };
36 } catch (err) {
37 this.status = 'error';
38 this.error = err.message;
39 }
40 },
41 },
42};
43</script>Without JavaScript at all
If you do not need inline states, a plain form works inside a Vue template too — the browser
posts it and Formboost answers with a 302 to your _redirect URL (which must be https://
and on the same host as the page):
1<template>
2 <form action="https://formboost.app/f/YOUR_ENDPOINT_ID" method="POST">
3 <input type="email" name="_replyto" required />
4 <textarea name="message" required></textarea>
5 <input type="hidden" name="_redirect" value="https://www.example.com/thanks" />
6 <input type="text" name="_honey" style="display:none" tabindex="-1" autocomplete="off" />
7 <button type="submit">Send</button>
8 </form>
9</template>Where the submission goes next
Email notification is on by default. On the Free plan you can swap it for a Slack, Discord or Telegram message instead — one destination per form; Starter allows five, so the same form can email you and post to a channel. Spam screening runs on every submission, and the troubleshooting guide covers what to check if a message does not arrive.