Blog

Vue 3 Contact Form Tutorial

Build a Vue 3 <script setup> contact form that posts to a FormBridge endpoint with fetch() and shows an inline success or error message via a reactive ref.

← Back to the blog
AI Powered

Explore this article with AI

Get an instant summary, key takeaways, action items, and answers to your questions about this article.

Choose your AI assistant
ChatGPT Perplexity
Quick answer

Build a Vue 3 <script setup> component with a form bound to @submit.prevent, then use fetch() to POST a FormData object to your FormBridge endpoint URL and update a reactive ref with the result — no server route or API layer is needed on the Vue side.

The component

Vue's <script setup> syntax makes it easy to track a submission status without much ceremony. Here's a full contact form component that posts to a FormBridge endpoint and reports back to the user without a page reload:

<script setup>
import { ref } from 'vue'

const status = ref('idle') // idle | sending | success | error

async function handleSubmit(event) {
  status.value = 'sending'
  const form = event.target
  const data = new FormData(form)

  try {
    const res = await fetch('https://app.formbridge.ai/api/forms/fb_8h2k9p', {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: data,
    })

    if (res.ok) {
      status.value = 'success'
      form.reset()
    } else {
      status.value = 'error'
    }
  } catch {
    status.value = 'error'
  }
}
</script>

<template>
  <form class="contact-form" @submit.prevent="handleSubmit">
    <label for="full-name">Full name</label>
    <input id="full-name" name="full-name" type="text" required />

    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />

    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>

    <button type="submit" :disabled="status === 'sending'">
      {{ status === 'sending' ? 'Sending…' : 'Send message' }}
    </button>

    <p v-if="status === 'success'" role="status">Thanks — we'll be in touch shortly.</p>
    <p v-if="status === 'error'" role="alert">Something went wrong. Please try again.</p>
  </form>
</template>

Why @submit.prevent plus fetch instead of a plain action

You could skip the script entirely and just set action="https://app.formbridge.ai/api/forms/fb_8h2k9p" on the form — that works too, and FormBridge doesn't care which method you use. The reason to reach for fetch in a Vue app specifically is that you already have reactive state available, so it's cheap to turn a full-page navigation into an inline success or error message instead.

The Accept: application/json header matters here: FormBridge returns JSON on success, so checking res.ok and updating status is enough to drive the UI — there's no response body you need to parse for a simple form like this.

Field names stay plain HTML

Nothing about using Vue changes what FormBridge expects. name="full-name", name="email", name="message" are ordinary attributes — FormBridge accepts any field name you send, and hyphenated names like full-name get auto-formatted into readable labels in your inbox and as merge tags ({{Full-Name}}) in notification emails.

Frequently asked

Do I need Vuex or Pinia to track a contact form's submit status?

No. A single ref() from the Composition API is enough to track idle, sending, success, and error states for a form this simple.

Should I use action= or fetch() to submit a Vue form to FormBridge?

Both work. A plain action attribute is simpler and needs no JavaScript, while fetch() lets you intercept the response and show an inline status message instead of a full-page navigation.

Why send an Accept: application/json header?

It tells FormBridge to return a JSON response instead of relying on a redirect, which makes it easy to check res.ok and update your Vue component's state directly.

Key facts

  • FormBridge accepts standard FormData from any <form>, so submitting via fetch() with new FormData(form) requires no manual serialization.
  • Sending an Accept: application/json header on the request returns a JSON body ({ ok: true, id: ... }) instead of an HTML redirect.
  • Field names are not validated against a predefined schema — any name attribute, including hyphenated ones like full-name, is accepted and stored as submitted.

Terms in this post

<script setup>
A compile-time syntax sugar in Vue 3 Single File Components that lets you write Composition API code directly in the <script> block without an explicit setup() function or return statement.

Get the next one in your inbox.

One email when we publish something worth reading. No spam — appropriately enough, we'd know.