Back to blog
    By

    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 fetch and reads the 202 response; the visitor never leaves the page.
    • Browser validation (required, type="email") runs first, so the handler only sees complete input.
    • A hidden _honey field catches bots; _replyto makes 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

    FieldEffect
    _replytoSets 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.
    _subjectThe notification email's subject line (up to 200 characters).
    _honeyThe honeypot. A submission with this field filled is stored as spam and never delivered.
    _redirectNot 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.

    Start with Formboost for free →